In questa sezione

Il formato binario di trasmissione: tipi di frame, codifica CBOR, semantica dei campi.

Formato di trasmissione

Ogni messaggio CapDAG è un frame CBOR con prefisso di lunghezza:

┌────────────────────────────────────────────────────┐
│  4 byte: lunghezza del payload, u32 big-endian     │
├────────────────────────────────────────────────────┤
│  N byte: mappa codificata in CBOR (chiavi intere)  │
└────────────────────────────────────────────────────┘

Il prefisso di 4 byte indica il numero esatto di byte del payload CBOR che segue. Il ricevente legge esattamente quel numero di byte, quindi decodifica la mappa CBOR.

Esistono due limiti di dimensione:

  • max_frame negoziato (predefinito 3.5 MB / 3,670,016 byte): impostato durante l’handshake. I frame più grandi vengono rifiutati sia da chi legge sia da chi scrive.
  • Limite rigido (16 MB / 16,777,216 byte): un tetto assoluto per evitare l’esaurimento della memoria. Non può essere negoziato al rialzo.

I payload che superano max_frame devono essere suddivisi in chunk (vedi Streaming).

Sorgente: capdag-rs/src/bifaci/io.rs (encode_frame, decode_frame, write_frame, read_frame).

Struttura del frame

Il payload CBOR è una mappa con chiavi intere. Ogni frame ha tre chiavi obbligatorie (version, frame_type, id). Tutte le altre chiavi sono facoltative e la loro presenza dipende dal tipo di frame.

Tabella di riferimento delle chiavi

Chiave Nome Tipo Descrizione
0 version u8 Versione del protocollo. Sempre 6. Qualsiasi altro valore viene rifiutato.
1 frame_type u8 Discriminante che identifica il tipo di frame (vedi sotto).
2 id bytes[16] o uint ID della richiesta. UUID per le richieste, uint per i frame di controllo.
3 seq u64 Numero di sequenza all’interno di un flusso. Assegnato da SeqAssigner in uscita.
4 content_type text Tipo di contenuto del payload, in stile MIME.
5 meta map Mappa di metadati con chiavi stringa. Il contenuto dipende dal tipo di frame.
6 payload bytes Dati binari del payload.
7 len u64 Numero totale di byte per un trasferimento a chunk. Impostato solo sul primo chunk.
8 offset u64 Offset in byte di questo chunk all’interno dello stream.
9 eof bool True sull’ultimo chunk di uno stream o sui frame END.
10 cap text Cap URN per i frame REQ. Identifica quale capacità invocare.
11 stream_id text UUID che identifica uno stream all’interno di una richiesta.
12 media_urn text Media URN che identifica il tipo di dati di uno stream.
13 routing_id bytes[16] o uint XID assegnato dal RelaySwitch per l’instradamento.
14 chunk_index u64 Indice, a partire da zero, di questo chunk all’interno del suo stream.
15 chunk_count u64 Numero totale di chunk in uno stream. Impostato su STREAM_END.
16 checksum u64 Hash FNV-1a a 64 bit del payload del chunk.
17 is_sequence bool Indica se il produttore ha usato emit_list_item (true) o write (false). Solo STREAM_START.
18 force_kill bool Indica se Cancel termina forzatamente il processo della cartuccia. Solo Cancel.
19 credit u64 Concessione di controllo di flusso in unità di CHUNK. Solo frame Credit.
20 unbounded bool Lo stream non promette alcuna lunghezza (L16). Solo STREAM_START.
21 output text L’output che uno stream di risposta è: il media URN di quell’output nella definizione della cap. Solo nello STREAM_START di una risposta, dove è obbligatorio.

Sorgente: capdag-rs/src/bifaci/frame.rs (modulo keys; struct Frame).

Tipi di frame

Il diagramma seguente mostra come i tipi di frame si relazionano all’interno del ciclo di vita di una richiesta:

stateDiagram-v2
    [*] --> Hello : impostazione della connessione
    Hello --> Req : connessione attiva

    state "Ciclo di vita della richiesta" as RL {
        Req --> StreamStart : apre uno stream
        StreamStart --> Chunk : invia dati
        Chunk --> Chunk : altri dati
        Chunk --> StreamEnd : stream completato
        StreamEnd --> StreamStart : apre un altro stream
        StreamEnd --> End : tutti gli stream completati
        Req --> End : risposta vuota

        Req --> Err : richiesta fallita
        StreamStart --> Err : richiesta fallita
        Chunk --> Err : richiesta fallita
    }

    state "In qualsiasi momento" as AT {
        Log : Log (avanzamento, info)
        Heartbeat : Heartbeat (sonda di salute)
    }

Hello (0)

Inviato una volta da ciascun lato durante l’impostazione della connessione. L’host invia per primo Hello (senza manifest); la cartuccia risponde con un Hello che include un manifest codificato in JSON in meta["manifest"]. Il manifest della cartuccia ha la struttura:

{
  "name": "mycartridge",
  "version": "1.0.0",
  "description": "...",
  "cap_groups": [
    {
      "name": "default",
      "caps": [{"urn": "cap:effect=none", "title": "Identity", ...}, ...],
      "adapter_urns": []
    }
  ]
}

La mappa meta porta anche le proposte di limiti: max_frame, max_chunk e max_reorder_buffer. I due lati negoziano i limiti prendendo il minimo delle due proposte. L’HELLO di una cartuccia porta inoltre la voce obbligatoria pools: byte JSON che codificano la sua mappa completa dello stato dei pool di concorrenza (un pool singleton per ogni cap registrata, ogni pool condiviso dichiarato nel manifest e il pool riservato all), ciascuno con declared/configured/available facoltativo/active/queued; vedi 13.1). Una mappa mancante o malformata è una violazione del protocollo, non un invito a dedurre un valore predefinito. L’heartbeat è il canale di CONFIGURAZIONE della capacità: una sonda può portare desired_capacities (byte JSON: nome del pool → valore configured dell’operatore), e ogni risposta all’heartbeat porta la mappa pools aggiornata obbligatoria. Un nome di pool sconosciuto fa rifiutare l’intero lotto desiderato con un ERR UNKNOWN_POOL al posto della risposta. Vedi Handshake per la sequenza completa dell’handshake.

Hello usa id = Uint(0) e non è associato ad alcuna richiesta.

Sorgente: frame.rs (Frame::hello, Frame::hello_with_manifest).

Req (1)

Avvia un’invocazione di capacità. Il campo cap (chiave 10) porta il cap URN da invocare. La richiesta può includere un payload e un content_type per i dati inline, anche se la maggior parte degli argomenti arriva come stream separati (STREAM_START/CHUNK/STREAM_END).

Il routing_id (chiave 13) viene assegnato dal RelaySwitch ai confini di instradamento. Le cartucce che inviano invocazioni peer non impostano routing_id; viene aggiunto dall’infrastruttura.

Il cap URN viene validato al momento della costruzione: Frame::req() va in panic se l’URN è malformato, perché un URN non valido è un errore di programmazione, non una condizione di runtime.

Sorgente: frame.rs (Frame::req).

Chunk (3)

Porta una porzione di dati in streaming all’interno di uno stream con nome. Campi obbligatori:

  • stream_id (chiave 11): lo stream a cui appartiene questo chunk.
  • chunk_index (chiave 14): posizione, a partire da zero, di questo chunk all’interno dello stream. Monotonamente crescente.
  • checksum (chiave 16): hash FNV-1a a 64 bit dei byte del payload. Obbligatorio per il rilevamento della corruzione.

Campi facoltativi impostati dal livello di suddivisione in chunk:

  • offset (chiave 8): offset in byte dei dati di questo chunk all’interno dello stream complessivo.
  • len (chiave 7): numero totale di byte. Impostato solo sul primo chunk (chunk_index = 0).
  • eof (chiave 9): true sull’ultimo chunk.
  • content_type (chiave 4): impostato solo sul primo chunk.

Sorgente: frame.rs (Frame::chunk, Frame::chunk_with_offset).

End (4)

Frame terminale che segnala che tutti gli stream di una richiesta sono completi. Nessun frame di flusso può seguire un END per lo stesso ID di richiesta (L4); i runtime scartano e contano i frame post-terminali, senza mai scriverli. Può portare un payload finale facoltativo.

Il campo eof è impostato a true. Un END riuscito porta exit_code = 0 in meta e PUÒ portare metadati terminali: progress (f64) e message. L’avanzamento terminale è l’evento di avanzamento finale autorevole (L5). Viaggia nel frame terminale stesso e quindi non può entrare in competizione con esso. Un END riuscito senza un progress esplicito vale come 1.0; un END fallito non ne sintetizza mai uno.

Sorgente: frame.rs (Frame::end).

Log (5)

Porta la diagnostica della sorgente e gli aggiornamenti di avanzamento. La mappa meta (chiave 5) contiene:

  • level (testo non vuoto): uno tra "info", "warn", "error", "progress" o un livello definito dalla sorgente.
  • message (testo non vuoto): messaggio leggibile da una persona.
  • attribution_class (testo, obbligatorio per i LOG non di avanzamento): uno tra input resource environment internal.
  • arg_urn (testo non vuoto, facoltativo per i LOG non di avanzamento): l’unico argomento a cui la sorgente attribuisce la diagnostica.
  • progress (float, facoltativo): un valore tra 0.0 e 1.0. Presente solo quando level è "progress".

I frame di avanzamento sono frame LOG con level = "progress". Vengono costruiti solo tramite l’API di avanzamento e non portano attribuzione diagnostica. Un LOG non di avanzamento deve portare un attribution_class valido; valori mancanti o sconosciuti sono violazioni del protocollo. arg_urn è presente solo quando la sorgente che emette sa che il messaggio riguarda un solo argomento. I riceventi non deducono mai nessuno dei due campi.

I frame LOG possono comparire in qualsiasi momento durante una richiesta, interfogliati con i frame CHUNK. Non influiscono sullo stream dei dati.

Sorgente: frame.rs (Frame::log, Frame::progress, log_level, log_message, log_progress, attribution_class, attribution_arg_urn).

Err (6)

Frame terminale che segnala un fallimento. Come per END, nessun altro frame può seguire per questa richiesta. La mappa meta porta:

  • code (testo non vuoto): codice di errore leggibile da una macchina.
  • attribution_class (testo): a quale dominio appartiene il fallimento, dichiarato nel punto di definizione o di emissione. Uno tra input resource environment internal.
  • message (testo non vuoto): descrizione dell’errore leggibile da una persona.
  • arg_urn (testo non vuoto, facoltativo): il media URN dell’unico argomento a cui la sorgente attribuisce il fallimento.

Tutti e tre i campi obbligatori sono rigorosi. Un attribution_class mancante o sconosciuto è una violazione del protocollo; i riceventi non fabbricano internal. Quando la sorgente non può attribuire il fallimento a un solo argomento, arg_urn è assente, mai vuoto. Vedi Gestione degli errori.

ERR ed END sono terminali mutuamente esclusivi. Una richiesta termina con esattamente uno dei due.

Sorgente: frame.rs (Frame::err, error_code, attribution_class, attribution_arg_urn, error_message).

Heartbeat (7)

Frame di monitoraggio dello stato di salute. Entrambi i lati possono inviare un Heartbeat; il ricevente deve rispondere con un Heartbeat che porta lo stesso id. Il CartridgeHostRuntime invia heartbeat ogni 30 secondi e si aspetta una risposta entro 10 secondi. Una cartuccia che non risponde viene contrassegnata come non in salute.

I frame Heartbeat aggirano la numerazione di sequenza perché non sono frame di flusso e non partecipano all’ordinamento.

Sorgente: frame.rs (Frame::heartbeat); vedi anche host_runtime.rs (HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT).

StreamStart (8)

Annuncia un nuovo stream con nome all’interno di una richiesta. Porta:

  • stream_id (chiave 11): un UUID che identifica in modo univoco questo stream all’interno della richiesta.
  • media_urn (chiave 12): il media URN (vedi Media URN) che identifica il tipo di dati dello stream.
  • output (chiave 21): su uno stream di risposta che non è l’output principale della cap, il media URN dell’output che questo stream è. Lo stream dell’output principale non porta output, e nemmeno uno stream di richiesta.

Una richiesta porta uno STREAM_START per ciascun argomento che le viene dato, ognuno etichettato con l’URN con cui la definizione della cap consegna quell’argomento (CapArg::stream_urn()), così che un op trovi ciascun argomento con un URN che dichiara. Uno stream di richiesta che porta output viene rifiutato dal runtime che lo legge: non è un argomento.

Una risposta porta al più uno STREAM_START per ciascun output dichiarato. Quello che non nomina alcun output è l’output principale della cap, l’output che dichiara il out= del cap URN: chi ha chiesto la cap ne legge la risposta senza possederne la definizione, che è ciò che fanno sia una cartridge che chiama un peer sia una riga di comando che stampa un risultato. Ogni altro stream nomina l’output che è. media_urn è sempre il tipo dei dati; output dice quale degli altri output della cap è uno stream, cosa che il solo tipo non può dire quando due output portano valori dello stesso tipo. Uno stream che nomina l’output principale, uno stream che nomina un output che la cap non dichiara e un secondo stream per lo stesso output sono ciascuno una violazione del contratto. Un output facoltativo che non scatta non ha alcuno stream. Vedi Input e output.

Sorgente: frame.rs (Frame::stream_start).

StreamEnd (9)

Segna la fine di uno stream specifico. Dopo questo frame, qualsiasi CHUNK per lo stesso stream_id è un errore di protocollo. Porta:

  • stream_id (chiave 11): lo stream che viene chiuso.
  • chunk_count (chiave 15): il numero totale di frame CHUNK inviati in questo stream, secondo il conteggio del mittente. Il ricevente può verificare di aver ricevuto tutti i chunk. Uno stream annunciato come unbounded non promette alcuna lunghezza e il suo STREAM_END omette chunk_count (L16).

Sorgente: frame.rs (Frame::stream_end).

RelayNotify (10)

Inviato da un RelaySlave al RelayMaster con cui è accoppiato per annunciare le capacità disponibili su quell’host. La mappa meta porta:

  • manifest (bytes): oggetto codificato in JSON con la struttura:
    {
      "caps": ["cap:effect=none", "cap:in=...;out=...;generate", ...],
      "installed_cartridges": []
    }
    

    caps è una lista piatta di stringhe di URN di capacità provenienti da tutte le cartucce in salute sull’host. installed_cartridges porta i metadati dei processi delle cartucce. CAP_IDENTITY (cap:effect=none) deve essere presente; in caso contrario il RelaySwitch rifiuta la connessione.

  • max_frame, max_chunk, max_reorder_buffer: limiti di protocollo per questa connessione di relay.

I frame RelayNotify vengono intercettati dal relay e non passano mai dall’altra parte. Il master memorizza il manifest e i limiti e li rende disponibili al RelaySwitch per le decisioni di instradamento.

Sorgente: frame.rs (Frame::relay_notify); vedi anche Topologia dei relay.

Credit (13)

Concede credito di controllo di flusso per stream al mittente di uno stream (vedi Controllo di flusso). Porta il RID della richiesta di destinazione nella chiave 2, uno stream_id facoltativo (chiave 11; se assente indica l’unico stream della richiesta) e il numero concesso in credit (chiave 19, unità di CHUNK). Gli intermediari inoltrano Credit da un capo all’altro come Cancel; non lo originano né lo assorbono mai (L11). Un frame CREDIT senza numero di crediti viene rifiutato in decodifica.

Cancel (12)

Annulla una richiesta in corso, identificata dal suo RID nella chiave 2 (e dal suo XID nella chiave 13 quando è instradata). Un Cancel agisce sempre: il runtime ricevente annulla la richiesta in modo cooperativo (chiude il suo input, rilascia chi è in attesa dei suoi crediti, annulla le sue chiamate peer e risponde con un ERR terminale una volta che l’handler è uscito), oppure, con force_kill (chiave 18), l’host termina il processo della cartuccia.

Un Cancel è attribuito esattamente come un frame ERR, tramite meta (chiave 5), nel vocabolario della tassonomia dei fallimenti (vedi Gestione degli errori). Ogni voce è facoltativa, e un Cancel non attribuito annulla comunque:

chiave meta significato che cosa riceve l’ERR terminale
code il codice terminale con cui terminerà la richiesta: CANCELLED (l’annullamento da parte di un operatore), ABORTED_COLLATERAL (un altro passo della stessa esecuzione è fallito), ABORTED (l’host ha deciso autonomamente) code; se assente → CANCELLED
attribution_class di chi è la decisione o la colpa dell’annullamento: user per l’annullamento da parte di un operatore; la classe del fallimento originario per uno smantellamento collaterale; la classe dell’host per un’interruzione da parte dell’host (environment per un’esecuzione obsoleta, resource per la pressione sulla memoria) attribution_class; se assente → internal (non classificato significa “nostro”)
message il motivo: il passo fallito per il caso collaterale, il motivo dell’host message; se assente → “Request cancelled”

CancelReason (frame.rs) è la forma tipizzata: user(force_kill), collateral(class, detail), host(class, detail, force_kill), unattributed(force_kill); Frame::cancel_reason() la rilegge da qualsiasi Cancel, senza mai fallire: un token di classe sconosciuto viene decodificato come “nessuna classe”, una meta mancante come non attribuito. Le cascate portano il motivo invariato (una chiamata peer di una richiesta interrotta è collaterale dello stesso fallimento), e una morte per terminazione forzata risponde a ogni richiesta in sospeso con il codice e la classe propri del motivo. “Annullato” significa quindi sempre che una persona ha annullato; uno smantellamento dovuto al fallimento di qualcos’altro lo dichiara ed è attribuito a quel fallimento.

Una richiesta termina una sola volta. L’END (o l’ERR) del suo handler e l’ERR di un annullamento sono alternative, mai entrambi: chi rivendica per primo la richiesta ne è il terminale. Un annullamento che arriva dopo che l’handler ha terminato la richiesta non aggiunge nulla, e una volta che un annullamento l’ha rivendicata il terminale proprio dell’handler non viene inviato. Qualsiasi frame per la richiesta dopo il suo terminale è un ritardatario post-terminale che chi scrive sopprime (L4). Un host in-process invia l’ERR dell’annullamento prima di chiudere l’input dell’handler, così un handler che si accorge per primo dell’annullamento non può terminare la richiesta alle proprie condizioni.

Un input troncato non è un input completo. Quando un annullamento (o la fine della connessione) chiude l’input di una richiesta, ogni stream ancora aperto, e il pacchetto di input stesso, fallisce con un errore di abbandono anziché terminare; una risposta peer troncata fallisce allo stesso modo. Un handler non può mai prendere una parte del suo input per l’intero input. Gli helper di accumulo (accumulate_input) rifiutano un input che non ha raggiunto il suo END o che ha portato un ERR.

Sorgente: frame.rs (CancelReason, Frame::cancel, Frame::cancel_reason), cartridge_runtime.rs (il ramo Cancel, TerminalClaim, i demux), in_process_host.rs, relay_switch.rs (cancel_request).

CloseStream (14)

Chiude gli stream di input dal vivo di una richiesta, la chiusura del rubinetto (15.2 §Le esecuzioni si arrestano). Porta il RID della richiesta di destinazione nella chiave 2 e, facoltativamente, lo stream_id (chiave 11) dell’unico stream di input il cui feed va chiuso; se assente indica ogni feed dal vivo detenuto dalla richiesta. Il runtime ricevente chiude il o i rubinetti dei feed corrispondenti: la cattura termina, l’handler esaurisce ciò che era già stato catturato e la richiesta termina naturalmente con END e output completi. Nulla viene annullato: lo stato della richiesta sul lato host resta intatto, nessun terminale viene sintetizzato e il frame non si propaga mai in cascata alle chiamate peer (il rubinetto riguarda solo la richiesta che porta il feed; i suoi peer si esauriscono naturalmente). Un CloseStream per una richiesta che non detiene alcun feed aperto corrispondente viene registrato nel log e ignorato; non diventa mai un’interruzione.

CloseStream è un frame di controllo non di flusso come Cancel e Credit (nessun seq, nessun buffering di riordino), instradato per RID→XID verso la destinazione della richiesta e inoltrato dagli intermediari senza interpretazione.

Sorgente: frame.rs (Frame::close_stream), cartridge_runtime.rs (il ramo CloseStream), relay_switch.rs (stop_request_feeds).

RelayState (11)

Inviato da un RelayMaster al RelaySlave con cui è accoppiato per fornire informazioni sulle risorse dell’host. Il payload (chiave 6) porta un blob opaco (codificato in CBOR o JSON dall’engine) che contiene lo stato delle risorse, come i percorsi dei modelli, la disponibilità della GPU o le richieste di capacità.

Come RelayNotify, i frame RelayState vengono intercettati dal relay e non passano mai.

Sorgente: frame.rs (Frame::relay_state).

Policy (15)

Ciò che l’host consente alle cartucce che serve, così com’è adesso. Viaggia in una sola direzione, dall’engine verso le cartucce, e una cartuccia non lo invia mai. Oggi dice una cosa sola, meta["network"]:

network Una cartuccia può raggiungere
offline nulla oltre questa macchina
no_telemetry i servizi di terzi e quelli di MachineFabric, mai l’analisi d’uso
full gli stessi, e l’analisi d’uso

Un Policy porta id = Uint(0), nessun seq, e non passa dal buffer di riordino. Un Policy senza network, o con qualsiasi altro valore, viene rifiutato alla decodifica.

Chi lo invia, e quando. L’engine dà al RelaySwitch la sua policy corrente. Lo switch la invia a ogni master come primo frame dopo il RelayNotify di quel master, prima di instradargli qualsiasi cosa, e a ogni master ogni volta che cambia. I relay la lasciano passare. Un CartridgeHostRuntime tiene l’ultima ricevuta e la invia a ogni cartuccia che ha collegato, prima della prima richiesta che le instrada, e a ogni cartuccia ogni volta che cambia. Un host che funziona senza switch (la riga di comando, un host in-process) riceve la sua policy da chi lo costruisce. Un host non è mai senza policy quando instrada una richiesta: uno che non l’ha ricevuta quando gli arriva una richiesta la rifiuta come violazione del protocollo.

Cosa ne fa una cartuccia. Il runtime tiene l’ultimo Policy e un handler lo legge dal vivo. Il valore della richiesta non è un’istantanea. Il codice che raggiunge la rete chiede prima di ogni chiamata, così un passaggio a offline ferma un download o un flusso di quotazioni in corso alla chiamata successiva. Una chiamata che la policy non consente non viene fatta: fallisce con codice NETWORK_POLICY, classe user, e un messaggio che nomina la policy. Un REQ che raggiunge una cartuccia prima di qualsiasi Policy è una violazione del protocollo, tranne la verifica di identità dell’host (CAP_IDENTITY), che fa parte dell’handshake e può precederlo: un host collega le sue cartucce prima di avere una policy da dare loro. Una cartuccia eseguita da riga di comando, senza host, è sotto full, perché chi la esegue decide per sé.

Una cartuccia che non raggiunge mai la rete non ha nulla da chiedere. Riceve comunque i frame Policy e tiene l’ultimo.

Sorgente: frame.rs (Frame::policy, HostPolicy), cartridge_runtime.rs (il ramo Policy, Request::network), host_runtime.rs (set_policy), relay_switch.rs (set_policy).

MessageId

Gli ID dei frame si presentano in due varianti:

  • Uuid: 16 byte casuali (UUID v4). Usato per gli ID delle richieste e degli stream, e per qualsiasi altra cosa debba essere univoca a livello globale.
  • Uint: un intero a 64 bit. Usato per i frame di controllo (Hello usa id = 0, Heartbeat può usare qualsiasi intero).

Gli UUID sono codificati come stringhe di byte CBOR (major type 2, 16 byte). Gli interi sono codificati come interi senza segno CBOR (major type 0).

Sorgente: frame.rs (enum MessageId).

Frame di flusso e frame di controllo

I frame sono classificati come di flusso o non di flusso. La distinzione è importante per la numerazione di sequenza e il riordino:

  • Frame di flusso: Req, Chunk, End, Log, Err, StreamStart, StreamEnd. Partecipano alla sequenziazione: il SeqAssigner assegna valori di seq monotonamente crescenti per flusso, e il ReorderBuffer ai confini dei relay li riordina se arrivano fuori sequenza.
  • Frame non di flusso: Hello, Heartbeat, RelayNotify, RelayState, Cancel, Credit, CloseStream, Policy. Aggirano completamente l’assegnazione della sequenza (il loro seq resta a 0) e non vengono bufferizzati dal buffer di riordino. Credit in particolare non deve mai accodarsi dietro i dati di cui regola il flusso.

Il metodo is_flow_frame() su Frame restituisce questa classificazione, e FrameType::is_terminal() indica quali frame di flusso terminano un flusso (End, Err).

Entrambe sono decisioni del modello dimostrato (formal/CapDAG/Bifaci/Flow.lean, FrameType.isFlow / isTerminal), generate in ogni mirror: nessun runtime mantiene una propria lista.

Sorgente: frame.rs (FrameType::is_flow, FrameType::is_terminal, Frame::is_flow_frame).

Checksum

Ogni frame CHUNK porta un checksum nella chiave 16: un hash FNV-1a a 64 bit dei byte del payload. Il mittente lo calcola tramite Frame::compute_checksum():

const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;

let mut hash = FNV_OFFSET_BASIS;
for &byte in data {
    hash ^= u64::from(byte);
    hash = hash.wrapping_mul(FNV_PRIME);
}

Il ricevente verifica il checksum dopo la decodifica. Una discrepanza è un errore di protocollo, e il frame viene rifiutato.

FNV-1a è veloce e semplice, non crittograficamente robusto. Rileva la corruzione accidentale (bit invertiti, troncamenti) ma non è progettato per resistere a manomissioni intenzionali.

Sorgente: frame.rs (compute_checksum).