← Documentation index Architecture › lifecycle

Metnos

lifecycle — one object for every change to the system
Canonical architecture.
Modules: runtime/change_intents.py, change_intent_adapters/,
change_applier.py, change_observer.py, change_rollback.py.

Audience: anyone operating the /admin/changes dashboard,
or wanting to understand where a proposal goes after accept.
Read time: 8 minutes.

Contents

  1. The unified object: change_intent
  2. Lifecycle in 5 acts
  3. The 6 sources as adapters
  4. The 3 daemons: materializer, applier, observer
  5. End-to-end example: find_recipes
  6. Operational references
proposed accepted applied observed finalized side branches: staged · rejected · failed · rolled_back
Figure 1 — Lifecycle of a change: a single state machine from proposal to finalization, with side branches (staged / rejected / failed / rolled_back).

1. The unified object: change_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.

2. Lifecycle in 5 acts

 +-----------+
 | 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).

3. The 6 sources as adapters

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.

SourceAdapterOutput kind
telos (10 lenses)telos.py create_executor (default) / extend_executor (parametric) / materialize_pipeline
introvertivaintrovertiva.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_feedbackuser_feedback.py reject_pattern (only if ≥ 2 rejections)

Score normalization per family:

4. The 3 daemons: materializer, applier, observer

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

Observer checks metrics post-applied_at and applies grace period (default 7 days, env METNOS_CHANGE_GRACE_DAYS). Rollback triggers:

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

5. End-to-end example: find_recipes

Telos engine SCAMPER suggests: «Extend find_files with kind=recipe to match .md files in ~/Documents/Recipes

ActStateWhere it happens
1. System creates ChangeIntentPROPOSED materializer @01:00 from telos_proposals.jsonl
2. User clicks ACCEPTED POST /admin/changes/{id}/accept
3. Applier modifies manifest + re-signAPPLIED change_applier @every_10m
4. 5 calls to find_files(kind=recipe), all OK OBSERVEDchange_observer @03:15 when age ≥ 0
5. After 7d trouble-freeFINALIZED 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.

6. Operational references


— aligned with the code.