Files
ThothII/docs/gestione-memory.md
T

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.

F1: chiarimento di un concetto
        │
        ▼
decisione concept_clarified nel ledger della sessione
        │
        ▼
F8: il reviewer decide se promuoverla
        │
        ├── registro globale registry.jsonl
        └── indice semantico pgvector
                    │
                    ▼
             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: harness/tht/memory.py e 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 pgvector 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.

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:

  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.py; il gate applica un ulteriore filtro difensivo in harness/.pi/extensions/tht-gate.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.

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.

pgvector

Dopo la promozione, save-one costruisce un solo VectorRecord e lo invia all'indice pgvector. 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 e concepts. L'hash SHA-256 del contenuto impedisce di ricalcolare embedding e upsert quando il testo non è cambiato.

Il comportamento è implementato in harness/tht/memory.py e harness/tht/memory.py.

Fonte canonica attuale

Oggi il registro JSONL è ancora la fonte canonica applicativa e pgvector è l'indice derivato. Il commento iniziale di memory_cmd.py segnala un debito tecnico: l'architettura futura prevista sarebbe usare direttamente il vector DB come archivio unico, ma questa migrazione non è ancora completata.

Riutilizzo in F2

In una sessione futura F2 esegue:

tht memory search "<domanda>" --session <id> --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.

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.

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. 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:

registro JSONL → pgvector → marker memory_promoted nel ledger

Se pgvector 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 pgvector e l'assenza di una transazione unica tra archivio globale, indice semantico e ledger della sessione.