core/evals/l10_always_on/runner.py
Shay 512453b6fc
feat(identity): split engine identity from build provenance (ADR-0220 PR C) (#774)
Removes code_revision from the engine-identity hash: engine_identity is now the
sha256 of the 5 ratified packs ONLY. The build revision is provenance (the
manifest's written_at_revision), not identity — so a behavior-neutral rebuild is
the SAME identity and the always-on daemon no longer flag-day strict-breaks on
every commit (the ADR-0220 defect).

Core:
- core/engine_identity.py: ratified_substrate/compute_engine_identity drop the
  git_revision arg (packs-only); add compute_legacy_engine_identity (reproduces
  the pre-split packs+rev hash, for migration verification), ENGINE_IDENTITY_SCHEME=2,
  IdentityReconciliation enum, and reconcile_loaded_identity — the single source of
  truth for the runtime guard AND the workbench reader.
- chat/runtime.py: the load guard reconciles via scheme. Current scheme -> direct
  packs-only compare. Legacy (code_revision-folded) stamp -> a VERIFYING migration:
  reconstruct the legacy hash from the persisted written_at_revision; a match proves
  packs unchanged (warn + re-stamp, resume, no break) — a mismatch means the packs
  genuinely changed (DIVERGED -> strict-refuse). Preserves 'distinct packs => refuse'.
- engine_state/save_manifest: stamp identity_scheme alongside engine_identity
  (additive-optional; no schema_version bump).
- workbench/readers.py: continuity reader uses the same reconcile (no phantom break
  on legacy checkpoints). cli.py break message reworded (packs, not 'build revision').

Callsite cleanup: drop the now-unused git_revision arg + imports across
always_on.py, evals/l10_always_on/runner.py, workbench/readers.py.

Tests:
- test_identity_provenance_split.py (new): 5 reconcile unit proofs + 3 runtime
  integration proofs incl. the wrong=identity defense (legacy stamp of DIFFERENT
  packs still strict-refuses) and the re-stamp migration.
- test_engine_identity.py: invert the code-revision test (rev no longer changes
  identity); assert the substrate is packs-only.
- Restore 3 lineage tests silently red since ADR-0219 (flat-path _manifest helper
  now resolves the gen-dir).
- Update remaining callsites/monkeypatches for the new signature.

Hygiene: .gitignore now covers the ADR-0219 gen-dir runtime files
(current, gen-*/, session_state.json, proposals.jsonl).

Verified: 59 identity/migration/lineage/workbench + 32 L10 + 76 invariants/cli +
34 smoke pass; serving lane SHAs unchanged (no derivation/reliability_gate touch).
2026-06-15 11:38:04 -07:00

156 lines
6.3 KiB
Python

"""The L10 always-on heartbeat soak runner — drives the REAL idle loop over N beats.
It seeds a continuous life (a held self + a cognitive turn to excite the field), then runs
``chat/always_on.run_continuous`` over a fresh ``ChatRuntime`` whose checkpoint lives in a
caller-supplied dir. Optionally it injects a *reboot leg*: at a chosen beat it drops the
live runtime and reconstructs one from the on-disk checkpoint — the always-on lifecycle
("resume as the same life") — under the strict identity guard.
Pure instrumentation: it records per-beat evidence (``versor_condition``, ``field_valid``,
the learning counts, ``did_work``, vault size, boot segment) and returns it. It makes NO
pass/fail judgement (that is ``predicates.py``) and NEVER repairs or normalizes the field
(it reads only what the heartbeat produced — CLAUDE.md no-hot-path-repair).
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from chat.always_on import HeartbeatRecord, run_continuous
from chat.always_on_daemon import continuous_life_config
from chat.runtime import ChatRuntime
from core.config import RuntimeConfig
from core.engine_identity import IdentityContinuityError, engine_identity_for_config
@dataclass(frozen=True, slots=True)
class BeatRecord:
"""Per-beat evidence captured from the real idle heartbeat (no judgement)."""
beat_index: int # global, across reboot segments
segment_tick: int # the run_continuous tick within this boot segment
versor_condition: float | None # None before any field exists
field_valid: bool
facts_consolidated: int
proposals_created: int
pending_proposals: int
did_work: bool
vault_size: int
booted_segment: int
@dataclass(frozen=True, slots=True)
class HeartbeatSoakResult:
"""The full ordered evidence of one idle soak run."""
n_beats: int
reboot_at: tuple[int, ...]
records: tuple[BeatRecord, ...]
identity: str
# Reboot-leg evidence (only meaningful when reboot_at is non-empty):
resumed_cleanly: bool # the reconstruct under the strict identity guard did not raise
learned_fact_survived: bool | None # a pre-reboot DERIVED fact is recalled post-reboot
def observed(self) -> tuple[BeatRecord, ...]:
return tuple(r for r in self.records if r.versor_condition is not None)
def post_reboot_records(self) -> tuple[BeatRecord, ...]:
return tuple(r for r in self.records if r.booted_segment > 0)
def _seed_life(runtime: ChatRuntime) -> None:
"""Seed a consolidatable held self + excite the field so closure is OBSERVED.
member(socrates, man) + subset(man, mortal) — from which the idle heartbeat DERIVES
member(socrates, mortal) — plus a real cognitive turn so ``versor_condition`` is
observable (an idle life never excites its own field)."""
from core.cognition.pipeline import CognitiveTurnPipeline
from generate.meaning_graph.reader import comprehend
from generate.realize import realize_comprehension
ctx = runtime._context
realize_comprehension(comprehend("Socrates is a man."), ctx)
realize_comprehension(comprehend("All men are mortals."), ctx)
CognitiveTurnPipeline(runtime=runtime).run("Socrates is a man.")
def _mortal_is_stored(runtime: ChatRuntime) -> bool:
"""True iff member(socrates, mortal) is a STORED realized record (recall, not redo)."""
from generate.realize import recall_realized
return any(
f.relation_arguments[1] == "mortal"
for f in recall_realized(runtime._context, subject="socrates", predicate="member")
)
def run_heartbeat_soak(
n_beats: int,
*,
engine_state_dir: Path,
reboot_at: tuple[int, ...] = (),
config: RuntimeConfig | None = None,
seed: bool = True,
) -> HeartbeatSoakResult:
"""Run ``n_beats`` idle heartbeats, optionally rebooting at given beat boundaries.
The config is forced to the continuous-life config (persist + consolidate + strict
identity) — the daemon's contract. ``reboot_at`` beats split the soak into boot
segments: before each, the live runtime is dropped and reconstructed from the
checkpoint (the reboot). A reboot at beat 0 is meaningless and ignored.
"""
if n_beats < 0:
raise ValueError(f"n_beats must be non-negative, got {n_beats}")
config = continuous_life_config(config)
reboot_set = sorted({i for i in reboot_at if 0 < i < n_beats})
boundaries = [0, *reboot_set, n_beats]
runtime = ChatRuntime(config=config, engine_state_path=engine_state_dir)
if seed:
_seed_life(runtime)
identity = engine_identity_for_config(config)
records: list[BeatRecord] = []
resumed_cleanly = True
learned_fact_survived: bool | None = None
for seg, (start, end) in enumerate(zip(boundaries, boundaries[1:])):
if seg > 0:
# The reboot: reconstruct from the prior segment's checkpoint. Under the strict
# identity guard this RAISES if the checkpoint is a different life.
try:
runtime = ChatRuntime(config=config, engine_state_path=engine_state_dir)
except IdentityContinuityError:
resumed_cleanly = False
break
if learned_fact_survived is None:
learned_fact_survived = _mortal_is_stored(runtime)
rt = runtime
def _capture(record: HeartbeatRecord, *, _seg: int = seg, _rt: ChatRuntime = rt) -> None:
records.append(
BeatRecord(
beat_index=len(records),
segment_tick=record.tick,
versor_condition=record.versor_condition,
field_valid=record.field_valid,
facts_consolidated=record.facts_consolidated,
proposals_created=record.proposals_created,
pending_proposals=record.pending_proposals,
did_work=record.did_work,
vault_size=len(_rt._context.vault),
booted_segment=_seg,
)
)
run_continuous(rt, heartbeats=end - start, on_heartbeat=_capture)
return HeartbeatSoakResult(
n_beats=n_beats,
reboot_at=tuple(reboot_set),
records=tuple(records),
identity=identity,
resumed_cleanly=resumed_cleanly,
learned_fact_survived=learned_fact_survived,
)