synapse — grafo, fitness, decadimento, potaturaUna volta nati, i neuroni vivono in un grafo emergente: i nodi sono i neuroni, gli archi sono le co-attivazioni reali. Questo documento definisce come il grafo viene costruito, aggiornato, letto, e come alimenta la legge darwiniana (fitness) che regola chi resta e chi viene potato.
neuron.html.synthesizer.html.memory.html (vocabolario diverso, non confondere).
Il grafo è costruito per aggregazione dai journal.sqlite di ogni
neurone (vedi neuron §6) e materializzato in un
DB SQLite centrale workspace/neurons/_graph.sqlite aggiornato da
un job periodico.
-- workspace/neurons/_graph.sqlite
CREATE TABLE nodes (
name TEXT PRIMARY KEY,
version TEXT NOT NULL,
state TEXT NOT NULL, -- neonato|attivo|dormiente|quarantena|estinto
fitness REAL DEFAULT 0.0, -- aggregato; vedi §3
invocations INTEGER DEFAULT 0,
last_invoked_at DATETIME,
last_success_at DATETIME,
strikes INTEGER DEFAULT 0, -- error streak recente
updated_at DATETIME NOT NULL
);
CREATE TABLE edges (
src TEXT NOT NULL,
dst TEXT NOT NULL,
declared BOOLEAN NOT NULL, -- da manifest.may_call
co_activations INTEGER DEFAULT 0, -- observed
last_co_at DATETIME,
weight REAL DEFAULT 0.0, -- vedi §4
PRIMARY KEY (src, dst)
);
CREATE INDEX idx_edges_src ON edges(src);
CREATE INDEX idx_edges_weight ON edges(weight DESC);
Un arco può essere:
declared=1, co_activations=0): permesso ma mai usato. Candidato a "sinapsi promessa ma vana".may_call → errore, viene loggata ma non genera arco (il loader di neuron solleva UndeclaredSynapseError).La fitness misura riduzione del gap nel tempo. Non "quante volte è stato invocato" ma "quanto ha avvicinato myclaw al suo scopo". È la quantità che decide visibilità nel retriever e, sotto soglia, potatura.
Il Gap viene calcolato dal caller (reasoning loop) secondo la
metrica associata al tipo di task:
| Tipo di task | Metrica Gap | Affidabilità |
|---|---|---|
| Concreto | Passi residui per raggiungere lo stato, exit code, tempo wall. | Alta (numerica). |
| Sfumato | Punteggio critic-LLM + feedback utente esplicito; asimmetria prudenziale (negativo pesa 2×). | Media. |
| Sconosciuto | Gap non misurabile → invocazione marcata u=null. | Zero. |
fitness(n) = sum over invocations i of:
u(i) * decay_factor(age_days(i))
+ exploration_bonus(n)
decay_factor(d) = exp(-d / TAU) # TAU = 30 giorni (Ebbinghaus-like)
exploration_bonus(n) =
B0 if state == "neonato" else 0 # B0 = 0.5, decade a 0 appena attivo
Le invocazioni con u=null non contribuiscono alla fitness ma
contano nel contatore invocations (per far uscire i neuroni dalla
fase "sconosciuto").
Il peso di un arco (src, dst) cresce con la co-attivazione e decade con il silenzio:
weight(src, dst) =
log(1 + co_activations) # saturazione sublineare: evita dominanza
* decay_factor(days_since_last_co)
* success_ratio(src -> dst) # frazione di co-attivazioni in trace con outcome=success
success_ratio penalizza coppie di neuroni che si invocano ma falliscono insieme.La scelta del doppio riferimento (Ebbinghaus per la forma, ACT-R per il rinforzo via access-count) è intenzionale:
Dato uno scopo g corrente, il retriever ritorna un piccolo insieme di
neuroni candidati ordinati. È il punto in cui il grafo parla al reasoning loop.
def retrieve(goal, top_k=5):
# 1. Candidati per similarità semantica
cands = vector_search(embed(goal), table="nodes_purpose", top=20)
# 2. Filtri duri
cands = [n for n in cands
if n.state in {"neonato", "attivo"}
and policy.allows(n, current_ctx)]
# 3. Scoring
scored = []
for n in cands:
s = w1 * similarity(n, goal) # relevance
+ w2 * normalize(n.fitness) # fitness
+ w3 * neighborhood_bonus(n, goal) # amici attivi
scored.append((s, n))
# 4. Bandit: quota esplorativa
if random() < EPSILON:
pick = ucb_choice(cands, exclude=best(scored))
return [pick] + sorted(scored, reverse=True)[:top_k-1]
else:
return sorted(scored, reverse=True)[:top_k]
| Peso | Ruolo | Default |
|---|---|---|
| w1 (relevance) | Similarità semantica purpose ↔ goal | 0.50 |
| w2 (fitness) | Successi passati | 0.30 |
| w3 (neighborhood) | Il neurone è connesso a neuroni già utili per goal simili | 0.20 |
| EPSILON | Frazione esplorativa (bandit) | 0.05 |
EPSILON, i
neuroni ad alta fitness iniziale occupano tutti gli slot per sempre
(rich-get-richer). Un neurone rarissimo ma indispensabile in situazioni di
nicchia non troverebbe mai spazio. Il bandit assicura che, ogni tanto, un
candidato sub-ottimale venga provato.
journal.sqlite dei neuroni in _graph.sqlite.edges..audit/neurons_extinct/YYYY-MM/ in sola lettura.
Forensica, reversibilità, e conformità alla Legge 3 (tracciabilità) esigono
che niente sparisca davvero.
from typing import Protocol
from dataclasses import dataclass
from datetime import datetime
@dataclass
class GraphNode:
name: str
state: str
fitness: float
invocations: int
last_invoked_at: datetime | None
strikes: int
@dataclass
class GraphEdge:
src: str
dst: str
declared: bool
co_activations: int
weight: float
last_co_at: datetime | None
@dataclass
class RetrievalResult:
candidates: list[GraphNode]
picked_by: Literal["score", "exploration"]
class SynapseGraph(Protocol):
async def rebuild(self) -> None:
"""Job notturno: riaggrega journal, ricalcola fitness e weight,
applica transizioni di stato, potatura archi."""
...
async def observe_invocation(
self,
neuron: str,
trace_id: str,
gap_pre: float | None,
gap_post: float | None,
outcome: str,
) -> None:
"""Hot-path: registra un'invocazione, aggiorna strikes e last_invoked_at.
Il ricalcolo pieno avviene nel job notturno."""
...
async def observe_co_activation(
self, src: str, dst: str, trace_id: str, success: bool,
) -> None: ...
async def retrieve(
self, goal_text: str, context: dict, top_k: int = 5,
) -> RetrievalResult: ...
async def transition(self, name: str, new_state: str, reason: str) -> None: ...
# Errori
class NeuronNotInGraphError(Exception): ...
class PolicyRetrievalVetoError(Exception): ...
| Aspetto | Scelta v1 | Motivazione |
|---|---|---|
| Storage grafo | SQLite _graph.sqlite | Stessa famiglia dei journal; join semplici per il retriever. |
| Vector search | Embedding su purpose + cosine in SQLite (BLOB + funzione custom) | Scala fino a migliaia di neuroni; se superiamo, passiamo a FAISS locale. |
| Aggiornamento fitness | Incrementale su hot-path (strikes, last_invoked_at) + batch notturno per il calcolo pieno | Bilancia freschezza e costo CPU. |
| Bandit | ε-greedy con ε fisso 0.05 | Semplice, robusto. UCB-like più sofisticato rimandato a v2 se serve. |
| Scheduler job | Cron interno del gateway | Evita dipendenza da cron di sistema. |
| Alternativa | Perché scartata (o rimandata) |
|---|---|
| Database a grafo dedicato (Neo4j) | Overhead infrastrutturale; query tipiche (top-K, scan per stato) sono felici con SQLite. |
| Fitness calcolata solo on-demand | Costo per ogni retrieve sale linearmente con la storia. Meglio aggregato notturno + delta incrementale. |
| Decay lineare (no Ebbinghaus) | Non riflette il comportamento osservato dell'uso sporadico-ma-utile; la curva esponenziale è più fedele. |
| Niente bandit (puro greedy) | Rich-get-richer porta a una library che si fossilizza attorno ai primi neuroni buoni. |
| UCB1 / Thompson sampling | Complesso da spiegare e tunare; ε-greedy basta per il volume di uso previsto. Pronti a passare se il retriever diventa il collo di bottiglia. |
Cancellazione dura (rm -rf) all'estinzione | Viola la Legge 3 (tracciabilità). Tutto va in .audit/. |
| HippoRAG personalized PageRank | In valutazione. Utile quando il grafo è grande (> 500 nodi). Oggi è over-engineering. |
| Invariante | Test |
|---|---|
| Invocazione aggiorna hot-path | observe_invocation(...) → nodes.last_invoked_at aggiornato entro 100ms, invocations++. |
| Strike si incrementa su error | Outcome=error → strikes+=1; outcome=success → strikes=0. |
| 3 strike → quarantena | 3 observe_invocation consecutive con outcome=error → state = quarantena dopo prossimo rebuild. |
| Fitness decade senza uso | Neurone senza invocazioni per 30 gg: fitness(t+30) < fitness(t) · 0.4 (per TAU=30). |
| Arco dichiarato-e-osservato | Dopo co_activation valida: edges row con declared=1, co_activations++, weight > 0. |
| Arco osservato fuori may_call bloccato | Tentativo di co_activation su coppia non dichiarata → UndeclaredSynapseError, nessun edge creato. |
| Potatura archi deboli | Edge con weight < 0.05 per > 60 gg → dopo rebuild non presente in edges. |
| Retriever filtra stato | Neurone in quarantena NON appare mai in RetrievalResult.candidates. |
| Retriever rispetta policy veto | Neurone con capability ritirata → escluso dai candidati, non solo de-rankato. |
| Bandit ha quota esplorativa | Su 10000 retrieve con la stessa query, ≥ 3% dei picked_by è "exploration". |
| Estinzione preserva file | Transizione quarantena → estinto: directory spostata in .audit/neurons_extinct/YYYY-MM/, file presenti, sola lettura. |
| Rebuild idempotente | Due rebuild consecutivi senza nuove invocazioni producono lo stesso stato (fitness, edges, weights). |
| Riferimento | Cosa abbiamo preso |
|---|---|
| Neuroni+Memoria v1.1 §4 | Definizione di Gap, utilità, fitness; caveat sulla self-valutazione. |
| Neuroni+Memoria v1.1 §5 | Sinapsi dichiarate/osservate, decadimento, potatura. |
| Ebbinghaus (1885) | Curva di oblio esponenziale come forma canonica del decay. |
| ACT-R activation (Anderson) | Rinforzo via access-count; log(1 + uses) come saturazione. |
| MemoryBank (Zhong et al. 2023) | Integrazione Ebbinghaus + rinforzo già applicata a LLM memory. |
| Generative Agents (Park 2023) | Retrieval come relevance × recency × importance. |
| Sutton & Barto (bandit) | ε-greedy come baseline robusto per selezione-vs-esplorazione. |
| Huang et al. 2023 | "LLMs cannot self-correct": preferire metriche oggettive alla self-valutazione. |
| Neuron §6 | Fonte: journal per-neurone che alimenta il grafo. |
myclaw — synapse microprogettazione v1.0 — 2026-04-22
Terzo doc dell'estensione neuroni. Prossimo: constitution.html.