# 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. ```mermaid 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"] ``` ```text 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. Implementazione principale: la façade [harness/tht/memory/](../harness/tht/memory/), con le policy riusabili in [harness/tht/memory/core.py](../harness/tht/memory/core.py), e l'adapter [harness/tht/cli/memory_cmd.py](../harness/tht/cli/memory_cmd.py). ## 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 clinici o organizzativi; - criteri di inclusione ed esclusione di una popolazione; - 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 esempio `mem-0001`; - timestamp, sessione e sequenza della decisione originale; - `type`; - `subject`; - `detail`; - `rationale`; - `question_context`; - `tables` e `concepts`. 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 con `COUNT(DISTINCT cod_paz)` per anno. Esempi non validi: - `fact_cardioversione` come memory approvata; - `dim_time` come 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](../harness/.pi/skills/tht-sessione/SKILL.md:217). ## Promozione alla fine della sessione: F8 Alla fine del workflow il gate `reviewer_memory_promote` esegue una preview deterministica: ```text tht memory promote --session --preview --json ``` La preview: 1. legge le decisioni effettive della sessione; 2. considera solo `concept_clarified`; 3. scarta le decisioni già promosse; 4. scarta le sequenze già rifiutate in F8; 5. deduplica contenuti equivalenti; 6. propone al massimo cinque candidati. Il codice applica il filtro e la deduplica in [harness/tht/memory/core.py](../harness/tht/memory/core.py); il gate applica un ulteriore filtro difensivo in [harness/.pi/extensions/gate/memory/index.js](../harness/.pi/extensions/gate/memory/index.js). Il reviewer vede un'unica checklist, preselezionata. Per ogni candidato: - selezionato: viene eseguito `tht memory save-one` e poi viene registrato `memory_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](../harness/.pi/extensions/tht-gate.js:1691). 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 è: ```text /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](../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: ```text tht memory search "" --session --json ``` Il comando: 1. crea l'embedding della domanda; 2. cerca nel vector store solo record `kind=memory`; 3. risolve ogni hit nel registro JSONL tramite il suo `ref`; 4. scarta record assenti dal registro; 5. scarta qualsiasi tipo diverso da `concept_clarified`; 6. esclude le memory già decise nella sessione corrente; 7. restituisce i risultati ordinati per similarità. L'implementazione è in [harness/tht/cli/memory_cmd.py](../harness/tht/cli/memory_cmd.py:368). 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_clarified` nella 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](../harness/tht/phase.py:82). ## 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:N` verso il `concept_clarified` originale; - nasconde marker il cui record originale non è `concept_clarified`; - nasconde soggetti che corrispondono a tabelle dello schema-linking; - nasconde soggetti autonomi con forma `fact_*` o `dim_*`; - mantiene invece i riferimenti a tabelle e campi quando fanno parte della spiegazione concettuale. La logica è in [harness/tht/session/store.py](../harness/tht/session/store.py:237). 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: 1. `REUSABLE_TYPES` nel core Python; 2. filtro del comando `memory search`; 3. filtro della preview F8; 4. filtro e deduplica nel gate Pi; 5. 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: ```text 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.