lifecycle — un solo oggetto per ogni cambiamento al sistemachange_intent
Tutti i cambiamenti al sistema convergono in un singolo schema in ~/.local/state/metnos/change_intents.sqlite:
id UUID
fingerprint sha256[:32] # deterministico per dedup fra sorgenti
state PROPOSED|ACCEPTED|APPLIED|OBSERVED|FINALIZED|
STAGED|REJECTED|FAILED|ROLLED_BACK
origin_family telos|introvertiva|synt|user
origin_module scamper|dedupe|request_new_executor|feedback|...
intent_kind create_executor|extend_executor|dedupe_executors|
reject_pattern
intent_target nome executor o pattern
intent_summary 1 frase rivolta all'utente
intent_body dict kind-specific (arg_name, tools_sequence,...)
score 0–1 normalizzato fra sorgenti
confidence 0–1
convergence N. sorgenti che propongono cose equivalenti
decision_* registro accept/reject/stage utente
applied_effect diff applicato + rollback_blob path
observed_metrics metriche periodo di grazia
Il fingerprint NON include origin_family: cosí
due sorgenti diverse che propongono cose equivalenti (es. telos:scamper +
synt sullo stesso executor) confluiscono in un solo record, con
convergence incrementato.
+-----------+ | PROPOSED | ← qualche sorgente l'ha generata +-----------+ / | \ / | \ (utente) | (utente decide piu' tardi) / | \ v v v +----------+ +--------+ +---------+ | ACCEPTED | | STAGED | |REJECTED | +----------+ +--------+ +---------+ | (daemon applier) v +----------+ +----------+ | APPLIED | ----> | FAILED | (nuovo tentativo possibile) +----------+ +----------+ | (daemon observer, periodo di grazia 7gg di default) v +----------+ +-------------+ | OBSERVED | ----> | ROLLED_BACK | (fisico, per kind) +----------+ +-------------+ | v +-----------+ | FINALIZED | ← consolidato, rimosso dalle viste default +-----------+
Da QUALSIASI stato si può transitare a ROLLED_BACK
(via di fuga per la revisione umana). Da REJECTED è possibile
riproporre (transizione a PROPOSED).
Ogni sorgente ha un adapter in runtime/change_intent_adapters/
che proietta i propri record in ChangeIntent, senza effetti
collaterali. Dal 2/7/2026 (ADR 0180, regola dei livelli) gli adapter attivi
sono quattro:
| Sorgente | Adapter | Kind output |
|---|---|---|
| telos (10 lenti, teste di cluster) | telos.py |
create_executor (new_valid) / extend_executor (parametric) / materialize_pipeline (existing_pipeline) |
| introvertiva (solo dedupe) | introvertiva.py |
dedupe_executors (le shape storiche generalize/specialize restano leggibili, i generatori sono ritirati) |
| synt (request_new_executor) | synt.py |
create_executor (importato come FINALIZED se già installato) |
| turn_feedback utente | user_feedback.py |
reject_pattern (solo se ≥ 2 rifiuti) |
L'adapter telos proietta le teste di cluster
(recompose_clusters) e non le righe grezze: circa 27 intenti al
posto di 500, punteggio = cluster_score (allineamento massimo +
bonus di convergenza fra lenti distinte), e SOLO i name_status
azionabili — il rumore (proposte ridondanti o con nome invalido) non arriva
alla vista.
Adapter ritirati (2/7/2026, «se un meccanismo non serve
non serve»): canonical.py (lo store canonical_query_log
era scritto dal planner storico, disattivato — il suo ruolo è assorbito
dal fastpath L0) e multi_tool.py (lo store
multi_tool_paths non ha più uno scrittore — le catene reali
le impara L1 autopath dai turni). I moduli restano nella storia di git; le
righe residue sono state respinte con ragione esplicita.
Raccolta unificata delle proposte (runtime/proposals_unified.py):
SUPPORTED_SOURCES = ("telos", "introvertiva") — le altre
sorgenti scrivono direttamente senza passare per il centro di raccolta.
Normalizzazione del punteggio per famiglia:
cluster_score (allineamento + convergenza), già 0–1n_seen*0.05 + last_uses*0.005 (formula storica)installed=0.8, abandoned=0.3,
rejected*=0.05min(0.9, 0.3 + 0.15*n_rejections)Promozione fastpath: i cluster fastpath possono essere
promossi a proposte executor tramite task_fastpath_promotion.
Le proposte risultanti sono visibili in /admin/changes.
| Daemon | Trigger | Cosa fa |
|---|---|---|
change_intent_materialize |
daily@01:00 | Concatena gli adapter vivi → upsert con dedup via fingerprint → bump convergence. |
change_applier |
every_10m | Legge ACCEPTED, applica fisicamente per kind. Cap 20/fire. |
change_observer |
daily@03:15 | Legge APPLIED+OBSERVED, calcola metriche, transition a FINALIZED o ROLLED_BACK. |
Applier handler per kind:
synth_request
esistente (~150s a cronometro). Idempotente: scorciatoia se già nel catalogo.change_applier_extend.py.
Append della sezione [args.properties.<arg>] in fondo al
manifest TOML, backup pre-modifica in
~/.local/share/metnos/rollback_blobs/<sha8>-<name>.toml,
re-sign via sign.sign_executor.executor_aliases.json + deprecate del duplicato in
executor_stats.db.suggested_query della proposta,
dentro scheduled_turn_scope: valgono il gate di consenso per
l'invio e le guardie del vaglio). L'esito onesto finisce in
applied_effect; se il turno funziona, L0/L1 imparano da soli dal
turno vero — niente depositi paralleli (ADR 0180).UPDATE canonical_query_log SET
state='active' (kind storico: la sorgente canonical è ritirata,
l'handler resta per le righe accettate in passato).~/.local/share/metnos/rejected_patterns.jsonl (agent_runtime
lo legge come HARD CONSTRAINT del planner).L'observer verifica le metriche dopo applied_at e applica un periodo di grazia (default
7 giorni, env METNOS_CHANGE_GRACE_DAYS). Inneschi del ripristino:
last_call_ok=False con ≥3 chiamate;
deprecato dall'executor_ager.demoted.Il ripristino fisico è delegato a change_rollback.py (per kind:
archivia la dir synth, ripristina il manifest dal blob + re-sign, rimuovi l'alias,
declassa lo stato, rimuovi la riga jsonl).
find_recipesIl motore SCAMPER di telos suggerisce: «Estendi find_files con
kind=recipe per far corrispondere i file .md in
~/Documents/Recipes.»
| Atto | Stato | Dove succede |
|---|---|---|
| 1. Sistema crea ChangeIntent | PROPOSED | materializer @01:00 da telos_proposals.jsonl |
2. Utente clicca ✓ | ACCEPTED | POST /admin/changes/{id}/accept |
| 3. Applier modifica manifest + re-sign | APPLIED | change_applier @every_10m |
4. 5 chiamate find_files(kind=recipe), tutte OK |
OBSERVED | change_observer @03:15 quando age ≥ 0 |
| 5. Dopo 7gg senza problemi | FINALIZED | change_observer al settimo passaggio |
Se invece dopo l'applicazione l'utente preme ✗ in chat su una
risposta che usa il nuovo arg, l'observer vede new_rejects≥2 per la query
interessata e transita a ROLLED_BACK — ripristino fisico
del manifest dal blob.
?limit=100|500).Accept: application/json (no text/html).python3 -m pytest runtime/tests/test_change_*.py -v
(55 test).python3 -m runtime.jobs.change_intent_materialize (idempotente).sqlite3 ~/.local/state/metnos/change_intents.sqlite.~/.local/share/metnos/audit/change_{intent_materialize,applier,observer}.jsonl.— allineato con il codice.