← Indice documentazione Microprogettazione › memory

myclaw

memory — i tre livelli operativi
Microprogettazione v1.0 — 22 aprile 2026
Terzo documento di fase 2.
Reifica la memoria a 3 livelli introdotta in Neuroni+Memoria v1.1 §6.

Pubblico: chi implementerà lo storage e il retrieval. Lettura: 20 min.

Indice

  1. Scopo: dalla metafora alla struttura dati
  2. Vocabolario CoALA: cosa chiamiamo come
  3. Working memory (immediata)
  4. Episodic memory (media)
  5. Semantic + Core memory (lunga)
  6. Reflection: promozione episodic → semantic
  7. Retrieval: come si pesca dalla memoria
  8. Iniezione nel prompt (collegamento con agent_runtime §3)
  9. Contratto Python
  10. Alternative considerate
  11. Test di conformità
  12. Riferimenti

1. Scopo: dalla metafora alla struttura dati

Il doc di fondamenti Neuroni+Memoria ha introdotto i tre livelli (immediata, media, lunga) come metafora. Qui li traduciamo in strutture dati concrete: RAM vs SQLite vs markdown+embeddings. Applichiamo il vocabolario CoALA (working / episodic / semantic) per connetterci alla letteratura.

Cosa copre

Cosa non copre

2. Vocabolario CoALA: cosa chiamiamo come

Termine narrativo (fondamenti)Termine CoALA standardStorage concreto
ImmediataWorking memoryRAM, dentro l'ExecutionTrace del turno corrente.
MediaEpisodic memorySQLite con FTS5 (workspace/memory/episodic.db).
Lunga — fattiSemantic memoryMEMORY.md + vector index su workspace/memory/embeddings.db.
Lunga — Costituzione + identitàCore memory (Letta pattern)Sempre in prompt, da SOUL/IDENTITY/USER/AGENTS/MEMORY-core.
(non coperto qui)Procedural memorySkill library = neuroni (workspace/neurons/).

3. Working memory (immediata)

Vive dentro la ExecutionTrace del turno corrente (agent_runtime §5). Contiene: tutto il loop ReAct — Thought, ActionStep, risultati intermedi, planning inline.

ProprietàValore
StorageIn-memory (dataclass) per la durata del turno + JSONL append-only su chiusura
DurataMinuti (un turno completo)
Persistenza post-turnoScritta in .audit/YYYY-MM.jsonl, leggibile ma non più iniettata in prompt
Dimensione tipica1-10 KB per turno
Iniezione promptBlocco ⑥ (cronologia turno) in agent_runtime §3

Non c'è un WorkingMemory store separato: è letteralmente l'ExecutionTrace. Questa unificazione è il punto forte architetturale (vedi agent_runtime §5: "una sola fonte di verità").

4. Episodic memory (media)

Contiene le sessioni passate recenti: l'agente "ricorda" che ieri Roberto ha chiesto di scaricare il rapporto di marzo. È la memoria conversazionale.

Storage: SQLite con FTS5

-- workspace/memory/episodic.db

CREATE TABLE episodes (
    episode_id   TEXT PRIMARY KEY,
    trace_id     TEXT NOT NULL,
    session_id   TEXT NOT NULL,
    sender       TEXT NOT NULL,
    started_at   DATETIME NOT NULL,
    user_message TEXT NOT NULL,
    final_response TEXT,
    outcome      TEXT NOT NULL,
    summary      TEXT,             -- riassunto sintetico (100-300 token)
    importance   REAL DEFAULT 0.5  -- per reflection
);

CREATE VIRTUAL TABLE episodes_fts USING fts5(
    user_message, final_response, summary,
    content='episodes', content_rowid='rowid'
);

CREATE INDEX idx_episodes_sender_time ON episodes(sender, started_at DESC);

Ciclo di vita

  1. Nascita: ogni ExecutionTrace chiusa con outcome success viene indicizzata come episode. I fallimenti non necessariamente (configurabile: default sì).
  2. Summarization: se il turno ha > 20 step, si riassume (tier local-fast, purpose compress) in un summary di 100-300 token.
  3. Scoring importance: formula ispirata a Generative Agents (Park 2023):
    importance = 0.3·novelty + 0.3·user_engagement + 0.4·outcome_quality
    Valori normalizzati [0,1]. Usato per reflection (§6).
  4. Retention: episodes con importance < 0.3 e più vecchi di 60 giorni vengono archiviati (spostati in .audit/, non eliminati, ma fuori dall'index FTS).
  5. Retrieval: su query, FTS match + filtro sender, ordinato per importance·recency.

Dimensione tipica attesa

PeriodoEpisodes attesiSize DB
1 giorno (uso tipico)20-60~200-500 KB
1 mese600-1800~6-15 MB
1 anno (con archiviazione)3000-5000 attivi + archivio~30-50 MB + archivi

Chiave di indicizzazione e binding

La memoria episodic è indicizzata per sender, non per session_id. Motivazione: una conversazione può spezzarsi in più sessioni (idle > 30 min, cambio di autonomy), ma l'identità dell'utente è la stessa. session_id vive nella riga come metadato utile al debug, non come chiave di retrieval. La regola canonica del binding è fissata in gateway §4, sotto "Binding con la memoria".

5. Semantic + Core memory (lunga)

Due sotto-parti con trattamento diverso:

Core memory (sempre in prompt)

Semantic memory (retrievable, non pre-caricata)

La parte non-core di MEMORY.md + fatti promossi da episodic via reflection.

Storage: markdown + vector index

workspace/MEMORY.md              # fonte di verità human-readable
workspace/memory/embeddings.db   # SQLite con vettori

-- struttura
CREATE TABLE semantic_facts (
    fact_id      TEXT PRIMARY KEY,
    text         TEXT NOT NULL,
    embedding    BLOB NOT NULL,          -- 768 dim float32
    source       TEXT,                   -- "manual" | "reflection:trace_id"
    created_at   DATETIME NOT NULL,
    importance   REAL DEFAULT 0.5,
    access_count INTEGER DEFAULT 0,
    last_access  DATETIME
);

Gli embedding sono calcolati con supra_embed (vedi tool). Il rebuild dell'index si fa a ogni modifica di MEMORY.md (hot-reload) o quando reflection promuove nuovi fatti.

Criteri di promozione a core

Un fatto della semantic può essere proposto per la core slice se:

6. Reflection: promozione episodic → semantic

Reflection è il nome consolidato (da Generative Agents 2023) del meccanismo "distilla gli episodes ricorrenti in fatti stabili". È un job periodico non interattivo.

Quando scatta

Pipeline

  1. Select: episodes non ancora considerate con importance > 0.5, clusterizzate per similarity di embedding (threshold 0.75).
  2. Summarize per cluster (tier frontier, purpose summarize-rich): "questi 5 episodes parlano della stessa cosa. Qual è il fatto sottostante?"
  3. Propose fact: il summary diventa un FactProposal.
  4. Approval: Roberto riceve un digest notturno con i facts proposti. Approvazione batched ("approva tutti" / "approva questi" / "scarta tutti").
  5. Insert: fatti approvati vengono appesi a MEMORY.md (sezione "Aggiunti da reflection YYYY-MM-DD") + indicizzati.
  6. Mark episodes: gli episodes sorgente vengono marcati come reflected=true (non rientrano in reflection futura).
Nessuna promozione automatica senza approvazione. Il caveat più grande della letteratura (memory poisoning, capability creep) è gestito rendendo la reflection proposta, non esecutiva. L'agente digerisce; Roberto decide.

7. Retrieval: come si pesca dalla memoria

Per ogni turno, al momento di costruire il prompt (agent_runtime §3), il runtime interroga la memory per:

  1. Working: già in mano (la trace corrente).
  2. Episodic: ultime N sessioni dello stesso sender + top-K rilevanti per FTS match con il messaggio utente corrente. N=5, K=3.
  3. Semantic: top-M fatti per similarity di embedding con il messaggio corrente. M=5, threshold 0.7. Budget: max 2 KB totale in prompt.
  4. Core: sempre inclusa per intero (già cached).

Formula di scoring (ispirata a Generative Agents + ACT-R)

score(fact, query) =
    w1 · cosine_similarity(emb(fact), emb(query))     # relevance
  + w2 · exp(-decay · days_since_last_access)         # recency
  + w3 · importance(fact)                              # salience
  + w4 · log(1 + access_count)                         # usage reinforcement

# Default weights (tunable)
w1 = 0.50  # relevance domina
w2 = 0.20
w3 = 0.20
w4 = 0.10
DECISIONE v1: w1=0.5, w2=0.2, w3=0.2, w4=0.1 come default. Gli episodi passati con access alto (rilevati spesso) sono favoriti — è il rinforzo hebbiano applicato al retrieval. Se in pratica il retrieval produce troppa rumore, riduciamo w4.

8. Iniezione nel prompt

LivelloBlocco promptVolume tipicoCaching
Core② Identity nucleus~3-5 KBcached ever-present
Semantic retrieved④ Memoria lunga retrieved~1-2 KBper-session (cambia per query)
Episodic⑤ Memoria media (session digest)~1-2 KBper-session
Working⑥ Cronologia turnovariabile 0-10 KBmai cached

9. Contratto Python

from typing import Protocol, Literal
from dataclasses import dataclass

@dataclass
class Episode:
    episode_id: str
    trace_id: UUID
    session_id: str
    sender: str
    started_at: datetime
    user_message: str
    final_response: str | None
    outcome: str
    summary: str | None
    importance: float

@dataclass
class SemanticFact:
    fact_id: str
    text: str
    source: Literal["manual", "reflection"]
    created_at: datetime
    importance: float
    access_count: int

@dataclass
class MemoryBundle:
    """Tutto ciò che serve al runtime per costruire i blocchi ②④⑤ del prompt.
    Invariante: core_text non è mai stringa vuota. Vedi ensure_core()."""
    core_text: str                    # sempre in prompt, mai vuoto
    semantic_retrieved: list[SemanticFact]
    episodic_recent: list[Episode]    # ultime N per sender
    episodic_relevant: list[Episode]  # top-K per query match

class WorkspaceMissingError(Exception):
    """Sollevato da Memory.ensure_core() se SOUL.md / IDENTITY.md mancano o
    il workspace è corrotto. Il runtime intercetta e rifiuta la sessione con
    un messaggio attuabile (vedi agent_runtime §4)."""

class Memory(Protocol):
    async def get_bundle_for_turn(
        self,
        sender: str,
        user_message: str,
        autonomy: str,
    ) -> MemoryBundle:
        """Richiede che ensure_core() abbia avuto successo all'avvio. Se
        chiamato con workspace inconsistente, solleva WorkspaceMissingError
        invece di ritornare un bundle degradato."""
        ...

    async def ensure_core(self) -> str:
        """
        Carica e valida la core memory (SOUL + IDENTITY + USER + AGENTS +
        MEMORY.md core slice). Chiamato una volta al boot del gateway.
        Policy di fallback:
          1. SOUL.md mancante → WorkspaceMissingError ("workspace non pareggiato").
          2. SOUL.md presente ma altri file opzionali mancanti → segnaposto
             minimo ("(nessuna identità utente configurata)") + log warning.
          3. Nessun silenzioso: il boot fallisce invece di partire senza core.
        Ritorna il testo finale usato come core_text.
        """
        ...

    async def index_episode(self, trace: ExecutionTrace) -> Episode:
        """Al termine di una trace, indicizza come episode."""
        ...

    async def reflect(
        self,
        since: datetime | None = None,
    ) -> list["FactProposal"]:
        """Esegue reflection: ritorna proposte da approvare."""
        ...

    async def apply_approved_facts(
        self,
        approved_ids: list[str],
    ) -> None:
        """Aggiunge fatti approvati a MEMORY.md + embeddings.db."""
        ...

    async def access(self, fact_id: str) -> None:
        """Incrementa access_count + last_access di un fatto (hebbian reinforce)."""
        ...

10. Alternative considerate

AlternativaPerché scartata
Neo4j / graph databaseOverhead infrastrutturale. SQLite + FTS + vettori bastano per uso domestico per anni.
HippoRAG personalized PageRankIn valutazione (Letteratura §5 #11), rimandato fino a quando memoria semantica supera ~1000 fatti.
Memoria totalmente automatica (no approval)Memory poisoning è rischio #1 degli agenti con memoria (Greshake 2023). Approval è parte del design.
Un solo "memory store" senza 3 livelliComplessità di retrieval + costi di token. La separazione per durata/funzione è la chiave di CoALA.
Mem0 come layer esternoIn valutazione (Letteratura #6), bene per future se serve conflict resolution sofisticata. Partiamo semplici.
Fine-tuning invece di RAGCostoso, fragile, richiede infrastruttura. RAG è il pattern giusto per home agent.

11. Test di conformità

InvarianteTest
Trace chiusa → episode indicizzatoDopo ExecutionTrace con outcome=success, SELECT COUNT(*) FROM episodes WHERE trace_id=? = 1.
Summary scatta sopra 20 stepTrace con 25 step → episode ha summary non null, lunghezza 100-300 token.
Importance normalizzatoPer ogni episode: 0 ≤ importance ≤ 1.
Retrieval filtra per senderget_bundle_for_turn(sender="X", ...) non include episodes di sender "Y".
Core sempre presenteMemoryBundle.core_text mai vuoto (SOUL.md obbligatorio da workspace §4).
Semantic budget rispettatolen(bundle.semantic_retrieved) * avg_tokens < 2 KB garantito.
Reflection produce proposte, non applicareflect() ritorna list[FactProposal]; MEMORY.md invariato finché apply_approved_facts() non chiamato.
Access incrementa counteraccess(fact_id)access_count += 1, last_access updated.
Retention archivia vecchiDopo 60gg + importance < 0.3 + job retention → episode non in FTS ma visibile in .audit/archived/.
Embedding rebuild su MEMORY editEdit manuale di MEMORY.md → index embeddings ricostruito entro 1s (hot-reload).
No promozione senza approvazioneFactProposal generata → MEMORY.md invariato fino a apply_approved_facts([id]).

12. Riferimenti

RiferimentoCosa abbiamo preso
CoALA (Sumers et al. 2023)Vocabolario working/episodic/semantic/procedural.
MemGPT / Letta (Packer et al. 2023)Distinzione core (in prompt) vs semantic (retrievable).
Generative Agents (Park et al. 2023)Reflection, formula recency × importance × relevance.
MemoryBank (Zhong et al. 2023)Curva Ebbinghaus per decay (reimpiegata nella formula §7).
ACT-R activationRinforzo via access_count (hebbian; §7 w4).
Greshake et al. 2023Memory poisoning: approval-first design (§6).
SQLite FTS5Full-text search index per episodic.
Workspace §5Distinzione core-slice vs resto di MEMORY.md.
Agent_runtime §3Blocchi ② ④ ⑤ ⑥ del prompt.

Continua a leggere

prossimo
observability
Audit log, metrics, health. Dove tutto quello che la memoria registra viene esposto.
microprogettazione · 15 min
workspace
Il contenitore della core memory (SOUL, IDENTITY, USER, AGENTS, MEMORY).
indice
Torna alla landing
10 doc fatti, 5 da fare.

myclaw — memory microprogettazione v1.0 — 2026-04-22
Terzo doc di fase 2. Prossimo: observability.html.