lifecycle — one object for every change to the systemchange_intent
All system changes converge into a single schema in ~/.local/state/metnos/change_intents.sqlite:
id UUID
fingerprint sha256[:32] # deterministic for cross-source dedup
state PROPOSED|ACCEPTED|APPLIED|OBSERVED|FINALIZED|
STAGED|REJECTED|FAILED|ROLLED_BACK
origin_family telos|introvertiva|synt|multi_tool|canonical|user
origin_module scamper|dedupe|request_new_executor|L1|L2|feedback|...
intent_kind create_executor|extend_executor|dedupe_executors|
materialize_pipeline|cache_pattern|reject_pattern
intent_target executor name or pattern
intent_summary 1 user-facing sentence
intent_body kind-specific dict (arg_name, tools_sequence,...)
score 0–1 cross-source normalized
confidence 0–1
convergence # of sources proposing equivalent things
decision_* user accept/reject/stage log
applied_effect applied diff + rollback_blob path
observed_metrics grace-period metrics
The fingerprint does NOT include origin_family:
this way two different sources proposing equivalent things (e.g.
telos:scamper + introvertiva:specialize on the same executor) coalesce into
a single record, with convergence bumped.
+-----------+ | PROPOSED | ← some source generated it +-----------+ / | \ / | \ (user) | (user decides later) / | \ v v v +----------+ +--------+ +---------+ | ACCEPTED | | STAGED | |REJECTED | +----------+ +--------+ +---------+ | (applier daemon) v +----------+ +----------+ | APPLIED | ----> | FAILED | (retry possible) +----------+ +----------+ | (observer daemon, default 7d grace period) v +----------+ +-------------+ | OBSERVED | ----> | ROLLED_BACK | (physical, per kind) +----------+ +-------------+ | v +-----------+ | FINALIZED | ← consolidated, hidden from default views +-----------+
From ANY state you can transition to ROLLED_BACK (human-audit
escape hatch). From REJECTED you can re-propose
(transition back to PROPOSED).
Historical sources are not deleted: they continue to exist as
«input feed» that powers the materializer. Each has an adapter
in runtime/change_intent_adapters/ that projects legacy records
into ChangeIntent.
| Source | Adapter | Output kind |
|---|---|---|
| telos (10 lenses) | telos.py |
create_executor (default) / extend_executor (parametric) / materialize_pipeline |
| introvertiva | introvertiva.py |
dedupe_executors / extend_executor (generalize) / cache_pattern (specialize) |
| synt (request_new_executor) | synt.py |
create_executor (imported as FINALIZED if already installed) |
| multi_tool_paths (L2) | multi_tool.py |
materialize_pipeline |
| canonical_query_log (L1) | canonical.py |
cache_pattern |
| user turn_feedback | user_feedback.py |
reject_pattern (only if ≥ 2 rejections) |
Score normalization per family:
expected_alignment already 0–1uses via log-saturating mid=20
(e.g. uses=20 → score 0.50, uses=100 → 0.83)n_seen*0.05 + last_uses*0.005 (historical formula)installed=0.8, abandoned=0.3,
rejected*=0.05min(0.9, 0.3 + 0.15*n_rejections)| Daemon | Trigger | What it does |
|---|---|---|
change_intent_materialize |
daily@01:00 | Concat 6 adapters → upsert with fingerprint dedup → bump convergence. |
change_applier |
every_10m | Reads ACCEPTED, applies physically per kind. Cap 20/fire. |
change_observer |
daily@03:15 | Reads APPLIED+OBSERVED, computes metrics, transitions to FINALIZED or ROLLED_BACK. |
Applier handler per kind:
synth_request
pipeline (~150s wall). Idempotent: short-circuits if already in catalog.change_applier_extend.py.
Appends [args.properties.<arg>] section at the end of the
TOML manifest, backup pre-modification in
~/.local/share/metnos/rollback_blobs/<sha8>-<name>.toml,
re-sign via sign.sign_executor.executor_aliases.json + deprecate the duplicate in
executor_stats.db.UPDATE multi_tool_paths SET
state='active'.UPDATE canonical_query_log SET
state='active'.~/.local/share/metnos/rejected_patterns.jsonl (agent_runtime
reads as HARD CONSTRAINT for the planner).Observer checks metrics post-applied_at and applies grace period (default
7 days, env METNOS_CHANGE_GRACE_DAYS). Rollback triggers:
last_call_ok=False with ≥3 calls;
deprecated by executor_ager.demoted.Physical rollback delegated to change_rollback.py (per kind:
archive synth dir, restore manifest from blob + re-sign, remove alias,
demote state, remove jsonl line).
find_recipesTelos engine SCAMPER suggests: «Extend find_files with
kind=recipe to match .md files in
~/Documents/Recipes.»
| Act | State | Where it happens |
|---|---|---|
| 1. System creates ChangeIntent | PROPOSED | materializer @01:00 from telos_proposals.jsonl |
2. User clicks ✓ | ACCEPTED | POST /admin/changes/{id}/accept |
| 3. Applier modifies manifest + re-sign | APPLIED | change_applier @every_10m |
4. 5 calls to find_files(kind=recipe), all OK |
OBSERVED | change_observer @03:15 when age ≥ 0 |
| 5. After 7d trouble-free | FINALIZED | change_observer on the 7th pass |
If instead after apply Roberto presses ✗ in chat on a
reply using the new arg, observer sees new_rejects≥2 for the target
query and transitions to ROLLED_BACK — physical restore
of the manifest from blob.
?limit=100|500).Accept: application/json (no text/html).python3 -m pytest runtime/tests/test_change_*.py -v
(55 tests).python3 -m runtime.jobs.change_intent_materialize (idempotent).sqlite3 ~/.local/state/metnos/change_intents.sqlite.~/.local/share/metnos/audit/change_{intent_materialize,applier,observer}.jsonl.— aligned with the code.