Ogni volta che scrivi un messaggio nella webchat di un brain ABChat e premi invio, parte una catena di passaggi che attraversa tre processi distinti su macchine logicamente separate. Questo documento ricostruisce quella catena dal punto di vista di chi l'ha costruita ma vuole il quadro consolidato in un posto solo, perché i pezzi sono cresciuti per stratificazione e oggi convivono quattro livelli diversi di informazione che raggiungono il modello. Capirli serve per due cose: sapere dove mettere mano quando qualcosa non va, e sapere quale leva tirare quando vuoi che il brain si comporti diversamente.
Il viaggio di una chiamata comincia nella webchat, l'interfaccia Laravel che gira nel container abc-php. Quando invii un messaggio, il controller non parla direttamente con il modello linguistico: passa per un intermediario chiamato hub, un piccolo servizio che vive nel container abc-hub e che espone un endpoint interno, /brain/exec. L'hub è l'unico componente che ha il diritto di entrare nel container del brain ed eseguire codice al suo interno.
Quando l'hub riceve la richiesta, fa una cosa concettualmente semplice ma architetturalmente importante: esegue docker exec abc-brain-{uid} claude -p, cioè lancia il binario di Claude Code dentro il container dedicato a quel brain, in modalità non interattiva. Il prompt viene passato via stdin, la risposta torna come stream di righe JSON che l'hub reinoltra a chi l'ha chiamato.
Qui c'è la prima distinzione che genera confusione, e vale la pena fissarla bene. Il container del brain e un processo a vita lunga: il suo PID 1 esegue tail -f /dev/null, un comando che non fa assolutamente nulla se non restare in attesa per sempre. E il pattern classico del container idle che sta li acceso ad aspettare. Ma il processo claude che risponde al tuo messaggio non e long-lived affatto: nasce quando arriva la chiamata, elabora il turno, scrive la risposta e muore. Al messaggio successivo ne nasce uno nuovo.
Il container vive, il processo claude muore a ogni turno. Confondere i due livelli porta a conclusioni sbagliate su cosa sia persistente e cosa no.
Questa e la ragione per cui un brain, interrogato su se stesso, puo dire correttamente di essere un container sempre acceso e contemporaneamente sbagliare dicendo di non essere un job che parte e muore. Sono vere entrambe a livelli diversi: il contenitore e permanente, l'esecuzione e effimera, e la memoria tra un turno e l'altro non vive nel processo ma nel filesystem del brain e nel database.
Stratificandosi nel tempo, il sistema ha finito per consegnare al modello quattro tipi distinti di informazione, ciascuno con una sua collocazione e una sua logica. Tenerli separati nella testa e la chiave per non perdersi.
Il primo livello e il payload HTTP che arriva all'hub. E il contratto piu esterno, il meno ambiguo: cinque campi che dicono a chi parlare e cosa chiedere. C'e lo slug del brain, che determina quale container verra invocato; il prompt, che pero a questo punto e gia stato arricchito a monte e non e il testo grezzo che hai digitato; il model, cioe quale modello usare; il resume_session_id, che serve a Claude Code per riprendere il filo di una sessione precedente; e il role, che distingue un utente normale da un amministratore. Questo livello e puro routing e identita: nessuna semantica, solo indirizzamento.
Il secondo livello sono le variabili d'ambiente che l'hub inietta nel docker exec. Qui si gioca una partita invisibile ma cruciale, quella dell'autenticazione e della privacy. Se il modello scelto e Claude e il brain ha le proprie credenziali OAuth, l'hub non imposta nessun ANTHROPIC_BASE_URL: il binario claude usera il file .credentials.json e parlera direttamente con l'endpoint reale di Anthropic, facendo gravare il costo sull'account personale dell'utente. Se invece il modello e DeepSeek, l'hub imposta ANTHROPIC_BASE_URL verso il gateway LiteLLM interno e gli passa la chiave master. In entrambi i casi vengono sempre settate tre variabili di igiene: CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DISABLE_TELEMETRY e DISABLE_ERROR_REPORTING, che impediscono al binario di telefonare a casa con dati di telemetria mentre l'utente magari sta usando un provider diverso proprio per non toccare Anthropic.
Il terzo livello e il piu ricco e il piu sottile: e il blocco di contesto che viene anteposto al testo dell'utente dentro il prompt vero e proprio. Qui vivono gli hint, istruzioni che il modello legge come parte del messaggio e che ne orientano il comportamento. Il runtimeHint gli dice quale modello e realmente, perche altrimenti tenderebbe a confondere l'identita narrativa del brain con il motore che lo fa girare. Lo skillsHint gli ricorda che le sue capacita vivono nelle sottocartelle di .claude/skills. Il brainHint gli ribadisce che la sua unica memoria persistente e il filesystem, e che deve cercare li prima di rispondere invece di fidarsi della propria memoria di modello. A questi si aggiungono, quando servono, l'indicazione del file che l'utente sta guardando e la lista degli allegati. Dopo gli hint vengono accodati lo storico della conversazione e finalmente il testo reale dell'utente. Tutto questo, impacchettato, e cio che il livello uno chiamava genericamente prompt.
Il quarto livello e il system prompt, passato a Claude Code con il flag --append-system-prompt. Quando il brain non ha un file CLAUDE.md che lo definisce, l'hub costruisce al volo l'identita concatenando i file di boot: il manuale di funzionamento, l'anima, il dominio operativo, il profilo dell'utente. E il livello che da al brain la sua personalita e le sue regole di fondo, distinto dal contesto contingente del singolo messaggio.
Fin qui la catena e la stessa per qualunque messaggio. La differenza tra esecuzione sincrona e asincrona nasce prima, nel modo in cui Laravel gestisce la chiamata, e si dissolve completamente quando si arriva all'hub.
Nella modalita sincrona, il controller della webchat apre una connessione HTTP e la tiene aperta per tutta la durata della risposta, facendo da semplice tubo: i pezzi di output arrivano dall'hub e vengono spinti immediatamente verso il browser. E diretto e immediato, ma fragile: se la connessione cade, se chiudi la scheda, se entri in galleria col telefono, quello che il modello stava producendo si perde, perche non e salvato da nessuna parte se non nel flusso che hai appena interrotto.
Nella modalita asincrona il disegno e diverso. La richiesta crea una riga in una tabella dedicata e mette in coda un job. Il job gira in background, indipendente dal tuo browser, chiama l'hub e man mano che la risposta arriva la accumula nel database invece di spingerla in una connessione. Dal lato del browser, un EventSource si collega a un endpoint che fa da lettore: interroga periodicamente il database e ti manda i pezzi nuovi, tenendo traccia di quanto hai gia ricevuto tramite un cursore. Se ti disconnetti e riapri, l'EventSource comunica l'ultimo punto raggiunto e il server riprende esattamente da li, senza buchi e senza ripetizioni. E robusto rispetto alle disconnessioni e adatto ai compiti lunghi, perche la fonte di verita non e la connessione ma il database.
Sincrono e asincrono divergono in Laravel e convergono nell'hub: a valle, il docker exec verso il container e identico. Il modello non ha modo di sapere da quale strada e arrivata la chiamata.
E proprio questa convergenza a creare il problema che ha motivato l'ultima modifica. Siccome l'hub esegue lo stesso identico comando in entrambi i casi, e il processo claude riceve solo il prompt e il modello, dall'interno del container non esiste alcun modo di distinguere se dietro c'e un browser in attesa in tempo reale o una coda che lavora in background. Il modello e cieco rispetto alla propria modalita di invocazione, e questo e corretto dal punto di vista dell'astrazione: rispondere a un prompt non dovrebbe richiedere di sapere chi e in ascolto. Ma a volte serve saperlo.
La soluzione adottata e coerente con l'architettura esistente invece di aggirarla. Dato che gli hint del terzo livello sono gia il canale con cui il sistema comunica al modello informazioni su se stesso e sul contesto, il modo naturale per dirgli che gira in modalita asincrona e aggiungere un sesto hint nel blocco di contesto. Non una variabile d'ambiente, che il modello dovrebbe andare a leggere di sua iniziativa, ma una riga di testo che legge naturalmente insieme a tutte le altre.
Questo hint viene inserito soltanto nel percorso asincrono. Dice al brain, in sostanza, che nessuno e in attesa in tempo reale, che l'utente potrebbe aver chiuso il browser, che la risposta verra comunque salvata e recuperata quando tornera, e che quindi puo prendersi il tempo che serve senza la pressione dell'immediatezza. Nel percorso sincrono questo hint semplicemente non c'e, e l'assenza stessa diventa l'informazione: nessun flag significa che qualcuno sta guardando in diretta.
E una scelta minima nel codice, una riga in piu nella costruzione degli hint condizionata al solo percorso asincrono, ma pulita nel principio. Il sistema non acquisisce un nuovo meccanismo di comunicazione: usa quello che gia esiste, lo estende di un caso, e mantiene la simmetria con gli altri hint che dicono al modello quale motore lo muove, dove vivono le sue capacita e dove sta la sua memoria. Il brain ora sa anche con che fretta deve rispondere, e lo sa nello stesso modo in cui sa tutto il resto.
Resta vero, e va tenuto a mente, che questa conoscenza e un dono esplicito del sistema, non una capacita del modello. Senza l'hint il brain resterebbe cieco, perche la modalita di invocazione vive a un livello dell'architettura che il processo claude non puo ispezionare. E il sistema a decidere cosa il brain deve sapere di se, e i quattro livelli sono esattamente i quattro canali attraverso cui questa decisione viene comunicata, turno dopo turno, a un processo che ogni volta nasce senza memoria del precedente.