11 KiB
Gestione delle memory
Questo documento descrive l'organizzazione attuale delle memory nel workflow ThothII: modello concettuale, ciclo di vita, persistenza, ricerca semantica, gate di revisione, visualizzazione delle sessioni e principali limiti tecnici.
Sintesi architetturale
Una memory è conoscenza di dominio riutilizzabile tra domande. Non è una copia dello schema-linking di una singola domanda.
flowchart TB
CLARIFY["F1 concept clarified"] --> REVIEW["F8 reviewer review"]
REVIEW -->|"accepted"| REGISTRY["registry.jsonl"]
REVIEW -->|"declined"| LOCAL["Session decision only"]
REGISTRY --> VECTOR["Qdrant semantic index"]
VECTOR --> FUTURE["Future F2 retrieval"]
FUTURE --> PROPOSAL["Reviewer proposal"]
F1: chiarimento di un concetto
│
▼
decisione concept_clarified nel ledger della sessione
│
▼
F8: il reviewer decide se promuoverla
│
├── registro globale registry.jsonl
└── indice semantico Qdrant
│
▼
F2 di una sessione futura
ricerca e proposta al reviewer
L'invariante principale è REUSABLE_TYPES = {"concept_clarified"}: le sole memory generabili, salvabili, ricercabili e proponibili sono i concetti chiariti. Le decisioni table_promoted, table_excluded, column_promoted e analoghe restano decisioni locali alla domanda.
I tre livelli della gestione
| Livello | Contenuto | Funzione |
|---|---|---|
| Ledger della sessione | concept_clarified, memory_promoted, memory_promotion_declined |
Audit e stato della singola sessione |
| Registro globale | Record mem-XXXX in registry.jsonl |
Archivio canonico attuale delle memory |
| Indice Qdrant | Embedding e metadati derivati dal registro | Ricerca semantica |
Il ledger contiene la provenienza e le decisioni umane. Il record globale contiene il testo riutilizzabile. L'indice vettoriale è una proiezione per la ricerca, non il posto in cui il workflow registra direttamente le decisioni.
Che cosa può diventare una memory
Durante F1 il workflow registra i chiarimenti come decisioni concept_clarified. Un chiarimento può esprimere:
- definizioni di concetti produttivi o organizzativi;
- criteri di inclusione ed esclusione di una linea di prodotto;
- formule e metodi di calcolo;
- interpretazioni temporali;
- mapping verso tabelle e colonne specifiche;
- significato di flag, codici o indicatori.
Il modello MemoryRecord contiene:
id, ad esempiomem-0001;- timestamp, sessione e sequenza della decisione originale;
type;subject;detail;rationale;question_context;tableseconcepts.
Per le nuove memory il tipo è sempre concept_clarified e tables viene inizializzato vuoto. Una tabella o una colonna può essere citata dentro la spiegazione come mapping tecnico; non può essere il concetto autonomo della memory.
Esempio valido:
Per ablazione si intende una procedura con
ablazione_transcatetere = TRUE, conteggiata conCOUNT(DISTINCT cod_paz)per anno.
Esempi non validi:
fact_cardioversionecome memory approvata;dim_timecome memory rifiutata;- una decisione “includi questa tabella” salvata per domande future.
La regola è documentata anche nella skill del workflow, in harness/.pi/skills/tht-sessione/SKILL.md.
Promozione alla fine della sessione: F8
Alla fine del workflow il gate reviewer_memory_promote esegue una preview deterministica:
tht memory promote --session <id> --preview --json
La preview:
- legge le decisioni effettive della sessione;
- considera solo
concept_clarified; - scarta le decisioni già promosse;
- scarta le sequenze già rifiutate in F8;
- deduplica contenuti equivalenti;
- propone al massimo cinque candidati.
Il codice applica il filtro e la deduplica in harness/tht/memory/core.py; il gate applica un ulteriore filtro difensivo in harness/.pi/extensions/gate/memory/index.js.
Il reviewer vede un'unica checklist, preselezionata. Per ogni candidato:
- selezionato: viene eseguito
tht memory save-onee poi viene registratomemory_promoted; - deselezionato: viene registrato
memory_promotion_declined; - nessun candidato: F8 si chiude automaticamente.
Il marker memory_promoted usa detail: seq:N, cioè un riferimento alla decisione concept_clarified originale. Il flusso è in harness/.pi/extensions/tht-gate.js.
La promozione non viene eseguita in F2 e il modello non può inventare candidati F8. I comandi diretti di promozione sono inoltre protetti dal gate anti-bypass.
Persistenza globale
Registro JSONL
Il registro attuale è:
<artifacts>/memory/registry.jsonl
La scrittura viene fatta tramite file temporaneo e os.replace, quindi la sostituzione del registro è atomica. L'idempotenza della promozione è basata sulla coppia session_id + decision_seq: la stessa decisione della stessa sessione non genera due record globali.
Qdrant
Dopo la promozione, save-one costruisce un solo VectorRecord e lo invia all'indice Qdrant. Il testo indicizzato include:
- tipo e soggetto;
- dettaglio;
- motivazione;
- domanda di contesto;
- eventuali concetti e mapping.
Il record vettoriale usa l'id memory:mem-XXXX, mentre i metadati conservano subject, detail, rationale, tables, concepts e il discriminante kind. L'hash SHA-256 del contenuto impedisce di ricalcolare embedding e upsert quando il testo non è cambiato.
Il comportamento è implementato in harness/tht/memory/core.py.
Fonte canonica attuale
Oggi il registro JSONL è ancora la fonte canonica applicativa e Qdrant resta un indice derivato ma persistente. Il workflow non registra direttamente le decisioni nel vector DB: usa Qdrant come proiezione interrogabile del registro e del ledger effettivo.
Riutilizzo in F2
In una sessione futura F2 esegue:
tht memory search "<domanda>" --session <id> --json
Il comando:
- crea l'embedding della domanda;
- cerca nel vector store solo record
kind=memory; - risolve ogni hit nel registro JSONL tramite il suo
ref; - scarta record assenti dal registro;
- scarta qualsiasi tipo diverso da
concept_clarified; - esclude le memory già decise nella sessione corrente;
- restituisce i risultati ordinati per similarità.
Le memory non vengono mai applicate automaticamente. Il modello deve presentarle in un'unica scelta reviewer_decide:
- una memory selezionata viene registrata come nuovo
concept_clarifiednella sessione corrente; - il rationale deve citare l'id originale
mem-XXXX; - una memory deselezionata significa “non applicarla ora”, non “cancellarla globalmente”.
Se F2 viene riaperta, una memory deselezionata può quindi essere proposta di nuovo. memory_rejected resta supportato per decisioni e sessioni legacy, ma non rappresenta il normale comportamento della deselezione F2 attuale.
Ledger effettivo, rollback e riaperture
Il ledger è append-only. Riaperture e ritrattazioni non cancellano le righe precedenti; cambiano però quali decisioni sono effettive.
Gli helper memory usano effective_decisions() per:
- escludere decisioni ritirate;
- ignorare decisioni appartenenti a fasi diventate stale dopo un rollback;
- impedire la promozione di chiarimenti non più validi.
La vista effettiva è definita in harness/tht/phase.py.
Visualizzazione nel riepilogo della sessione
La sezione “Memories” del riepilogo viene proiettata a runtime dal ledger della sessione; non è una copia diretta del registro globale.
La proiezione:
- mostra prima le memory approved;
- mostra poi le memory declined;
- risolve
seq:Nverso ilconcept_clarifiedoriginale; - nasconde marker il cui record originale non è
concept_clarified; - nasconde soggetti che corrispondono a tabelle dello schema-linking;
- nasconde soggetti autonomi con forma
fact_*odim_*; - mantiene invece i riferimenti a tabelle e campi quando fanno parte della spiegazione concettuale.
La logica è in harness/tht/session/store.py. Il rendering frontend usa una lista strutturata e tratta subject, detail e rationale come Markdown, evitando di mostrare il markdown grezzo.
La trasformazione avviene in lettura: anche le sessioni storiche vengono organizzate con il layout e i filtri correnti senza riscrivere gli artefatti originali.
Invarianti applicate
Le protezioni sono distribuite su più confini:
REUSABLE_TYPESnel core Python;- filtro del comando
memory search; - filtro della preview F8;
- filtro e deduplica nel gate Pi;
- esclusione delle table-memory nella proiezione UI.
Questo evita che una singola modifica al prompt o a un solo componente reintroduca le tabelle come memory.
Limiti e rischi residui
Registro e indice non sono una singola transazione
Il salvataggio segue sostanzialmente questa sequenza:
registro JSONL → Qdrant → marker memory_promoted nel ledger
Se Qdrant non è disponibile, il registro può contenere una memory non ancora ricercabile; il comando segnala che sarà necessario reindicizzare.
Se il marker del ledger fallisce dopo il salvataggio nel vector DB, la memory può risultare globalmente presente ma senza audit completo nella sessione. Il gate restituisce un comando di recupero manuale.
Limite di cinque candidati
F8 propone al massimo cinque memory. Se una sessione produce più di cinque concetti validi, gli elementi eccedenti non vengono mostrati e la sessione può essere finalizzata senza promuoverli.
Deduplica non globale
La deduplica impedisce duplicati nella stessa proposta e l'idempotenza impedisce di ripromuovere la stessa decisione. Non esiste però una fusione globale di due memory semanticamente simili provenienti da sessioni diverse.
Vecchi record fisici
Vecchi record table_promoted o table_excluded possono ancora esistere in artefatti o indici storici. Il codice attuale li rende non riutilizzabili filtrandoli per tipo e non li mostra nella proiezione delle sessioni. La loro eventuale rimozione fisica dal vector DB resta un'attività di bonifica separata.
Valutazione finale
La gestione attuale è coerente con il requisito funzionale: una memory è una conoscenza concettuale riutilizzabile, non una scelta di schema-linking.
La parte più solida è la difesa multilivello del tipo concept_clarified. Il principale debito tecnico riguarda invece la convivenza del registro JSONL con Qdrant e l'assenza di una transazione unica tra archivio globale, indice semantico e ledger della sessione.