core/evals/l10_always_on/runner.py
Shay aed273b14a test(l10): falsifiable long-horizon soak for the always-on IDLE heartbeat
The always-on PROCESS shipped (run_continuous + daemon + CLI), but the claim "it holds
over uptime" was UNFALSIFIED: the existing evals/l10_continuity soak drives the TURN loop
and is disjoint from run_continuous; the daemon path was covered only by ≤5-beat unit
tests. This lane converts "built" → "proven over horizon" for the idle path.

evals/l10_always_on/ mirrors the l10_continuity predicate/mutation harness but drives the
IDLE heartbeat: it seeds a real continuous life (held self + a cognitive turn to excite the
field), runs N beats with NO user turn, and gates four falsifiable predicates, each with a
*_holds (real soak) AND a *_bites (mutation) test per CLAUDE.md schema-as-proof:

- H1 closure — every observed idle beat versor_condition < 1e-6 (held over uptime, READ
  never repaired); bites on a breached beat AND on a vacuous no-field soak.
- H2 bounded idle — a no-work beat adds NOTHING to the vault (no idle leak — invisible at
  5 beats, fatal at 100k); bites on idle vault growth.
- H3 convergence — a saturated idle life SETTLES and stays settled (no re-awakening), at
  rest at the end, closure intact on the tail; bites on never-settling + on re-awakening.
- H4 reboot resume — a reboot mid-soak resumes the SAME life (strict identity guard passes,
  pre-reboot DERIVED learning survives, post-reboot closure holds); bites on failed
  resume + on lost learning.

Measured (5000 idle beats, reboot@2500, ~20s): ALL gates pass — versor_condition flat at
1.389e-07 (no drift, no repair), vault bounded at 6 (no idle leak), converged at beat 1
with a 4999-beat at-rest tail, reboot resumed the same life with learning intact. This is
the empirical resolution of the L10 riskiest-unknown FOR THE IDLE PATH (the closure-by-
construction ruling covered the field-transition walk; this covers indefinite idle uptime).

Honestly NOT covered (recorded in the report, no silent skip): the continuously-LEARNING
life's resource cost under a sustained new-fact stream (O(n²) snapshot; per-run lived_life)
— out of scope until an afferent/intake feed + incremental persistence exist (the next
frontier).

Run on demand: PYTHONPATH=. python -m evals.l10_always_on [n_beats] [reboot_beat]. Path-
scoped out of CI smoke (like l10_continuity); runs in post-merge full-pytest. Additive — a
new eval lane, no existing files touched. 12/12 lane tests + architectural invariants green.
2026-06-14 18:20:48 -07:00

157 lines
6.4 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
from engine_state import get_git_revision
@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, get_git_revision())
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,
)