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.
157 lines
6.4 KiB
Python
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,
|
|
)
|