Skip to main content

run_context_evolution

Function run_context_evolution 

Source
pub async fn run_context_evolution(
    engine: &Arc<Mutex<MemgineEngine>>,
    state: &Arc<ServerState>,
    dry_run: bool,
    pending: &Mutex<Vec<Value>>,
    backoff: Option<(&Mutex<ContextBackoff>, u64)>,
    measure: Option<(&dyn HarnessMeasurer, &HarnessMeasureRequest)>,
) -> Result<EvolutionOutcome, String>
Expand description

The Context arm of the evolution executor — the pillar’s real mechanism (car_memgine::context_evolution), replacing the not_executable error that used to make a documented boundary read as a failing subsystem.

The shape mirrors the Harness arm in handler.rs: diagnose from live signals, fingerprint each mutation, resolve it against the daemon’s SHARED durable approval ledger (approve on one connection, apply on another, survives a restart), and report a per-mutation detail object plus a summary.

Authorization resolves most-binding-first, over two paths. A durable operator decision always wins; only a mutation nobody has decided on reaches the pre-activation gate; only a mutation the gate could not run on (or could not decide) reaches the human gate:

  1. Rejected in the ledger → rejected_by_operator. An operator’s “no” is the most binding thing in the system and is never re-litigated by a measurement.
  2. Approved in the ledger → the human-approved path below, unchanged: backoff check, dry-run, then the one-lock baseline-compact / apply / re-compact / revert-unless-positive margin measurement.
  3. No prior decision and a grade is runnable — a measurer and a context_measure request were handed in, this is not a dry_run, and the mutation carries a patch that car_memgine::context_evolution::requires_human_approval says is gradeable → the pre-activation gate. Read the live car_memgine::MemgineConfig off the engine, replay the split twice (baseline under the live config, candidate under the live config plus the patch — see measure_context_baseline / measure_context_candidate), and hand both documents to car_memgine::harness_evolution::EvolutionAgent::evaluate_context, which is literally the same gate, with the same guards, that grades a harness mutation. Promote applies for real (applied, governance: "promoted"); Reject applies nothing and reports rejected_by_gate; NeedsApproval / Incomparable fall through to pending_approval carrying the gate’s own reason; a replay error reports measurement_failed and applies nothing; and a Promote whose measured base moved before the apply reports config_moved_during_measurement and also applies nothing.
  4. Anything else → pending_approval, with a reason naming the precondition that was missing ([context_pending_reason]).

Three properties of that resolution are load-bearing:

  • rejected_by_gate does NOT fall through to the human gate. It is a verdict, not an absence of one: the daemon measured this exact change on task outcomes and it came back a regression. Listing it for approval would invite an operator to approve a change the gate had just measured as worse — and because the ledger is daemon-wide and keyed on the change, that approval would then stand on every engine, permanently, over the top of a real measurement. An operator who disagrees can still approve the fingerprint directly through permission.approve; what must not happen is the daemon soliciting it.
  • A measurement_failed mutation is not falsified. The measurement was. ContextBackoff::note_falsified is deliberately NOT called on that path: backing a mutation off because the bench errored would punish the change for an infrastructure failure and delay the retry that would have graded it honestly.
  • A promotion is re-checked against the live config before it applies. The engine lock is DROPPED across the two replays, so the config the grade was measured under can move before the apply — another session’s evolution.run over the same engine, the human-approved path, a cadence tick. Under the same lock hold that would apply the patch, the fields a car_memgine::ContextConfigPatch can reach are compared against the config the baseline ran under (context_patch_base_moved). If they moved, the step reports config_moved_during_measurement carrying both values and applies NOTHING: the verdict was computed against a base that no longer exists, and the inverse patch apply_context_patch hands back would describe the CURRENT value rather than the measured one, so even the rollback the contract promises would restore the wrong config. Like measurement_failed, this records NO backoff — the measurement was invalidated, not the change — and the correct recovery is a later cycle re-diagnosing and re-measuring against the new base.

The post-apply margin measurement is retained, unchanged, on the human-approved path (2) — defence in depth, and the only automatic check on a change an operator authorized without asking for a grade:

  • On that path the measurement happens AFTER the apply, and it measures the MARGIN. Under ONE lock acquisition on the engine — otherwise another task’s ingest would be credited or blamed — the arm compacts under the unchanged conversation_keep_recent first (conversation_tokens_baseline), then applies the patch, compacts again, and re-reads (conversation_tokens_after). Comparing against that baseline rather than against the uncompacted layer is the load-bearing part: the uncompacted comparison would credit the mutation with every token compaction was going to save anyway, and on a change an operator authorized without asking for a grade this is the ONLY automatic check on it. If the margin is not positive, the contract predicted something that did not happen, so the inverse patch goes back on inside the same lock hold and the step reports rolled_back (or rollback_failed, its own status, when even that does not take) and counts nothing as applied. Note what a rollback does and does not restore: the config knob goes back, the summarization performed while measuring does not — compaction replaces turns with summaries and keeps them in the layer, which is what the engine’s own heuristic does at this saturation anyway.
  • backoff is Some only on the unattended cadence. A falsified mutation restores the knob, so the next tick re-diagnoses it, re-matches the same standing approval and repeats the whole apply-measure-revert round under the engine lock — forever. ContextBackoff is that brake, keyed per fingerprint, mirroring SkillsBackoff (kernel review S5). A session-driven evolution.run passes None: a person asking for the check now should get it now.
  • measure is Some only when the caller supplied context_measure AND this build has an in-process measurer installed. The unattended cadence passes None on purpose: a timer that started spending benchmark replays because someone set evolution_interval_secs would turn an opt-in cost into a background one. dry_run is honoured here rather than by the caller so the pending reason can say which precondition was missing — a dry run that reports “you did not ask for a grade” would be lying.