core/tests/test_l10_always_on.py
Shay 7ead395eeb feat(workbench): Lived Life surface — the always-on heartbeat made felt (read-only, persist-first)
The L10 heartbeat (chat/always_on, #747) holds CORE alive over uptime but had no
surface. This adds the read-only Lived Life route that renders the continuous-life
evidence: the heartbeat over uptime + the resume-as-same-life verdict — both halves
of "one continuous life" on one surface.

Persist-first, never recompute engine-owned values:
- chat/always_on: serialize_report + write_lived_life persist a run deterministically
  (sorted keys) to engine_state/lived_life.json; run_continuous gains an opt-in
  report_path so a real continuous-life run leaves its evidence where the workbench
  reads it. closure_ceiling is persisted so the artifact is self-describing.
- workbench/lived_life.py: projection + a fail-closed validate() gate — the wrong=0
  analogue for the continuity surface: closure_held/closure_observed/totals/converged
  are each re-checked against the per-beat measurements, and resume_status is re-checked
  against identity-vs-current-substrate, so a tampered artifact RAISES rather than
  rendering a false claim (a beat lying about a breached ceiling, an inflated closure,
  a miscounted total, a falsely-claimed resume).
- workbench/readers.lived_life(): reads the artifact, computes the resume verdict from
  the persisted identity vs the canonical engine_identity_for_config (fail-soft ->
  "unknown", like IdentityContinuity). Honest absence when no run has been persisted.
- GET /lived-life + LivedLife/LivedLifeHeartbeat schemas (snapshot regenerated; the
  schema-drift gate proves every field is mirrored in src/types/api.ts).

The resume verdict made felt: would_resume / substrate_changed / unknown IS the L11
reboot guarantee (a reboot recomputes identity and refuses if it differs) — so the
surface shows this life wakes up as ITSELF, not a copy. The per-reboot lineage chain
stays owned by Runs > Identity (honest cross-link).

Frontend: /lived-life route (Evidence section, route count 15 -> 16). Renders the
headline (heartbeats, closure-held-by-construction, learned-while-idle, at-rest, resume
pill), the summary, the heartbeat timeline (closure flat below the ceiling — read, never
repaired), and the resume verdict. Honest absence + ADR-0162 route conformance
(loading/error/empty), fail-closed like Vault/Calibration.

Tests (non-vacuous): 12 backend (every tamper case raises; resume tracks identity vs
substrate; run_continuous(report_path) round-trips a readable+valid artifact) + 4
frontend render tests (recorded life renders; a breached beat shows the warning, never a
false "held"; a changed substrate shows would-refuse, never a false resume; absent shows
honest absence). All workbench Python (167) + affected vitest (167) + tsc + vite build
green; architectural invariants green.

engine_state/lived_life.json is runtime state (gitignored, ADR-0146 pattern).
2026-06-14 16:45:03 -07:00

135 lines
6.7 KiB
Python

"""L10 — the always-on heartbeat: one continuous life that lives + learns when idle
and survives interruption as the SAME life.
These exercise the T-experience heartbeat (``chat/always_on.run_continuous``) directly:
the engine ticks ``idle_tick`` over uptime with no user turn, the closure invariant holds
by construction (read as evidence, never repaired), and a reboot resumes the SAME life
with its accumulated heartbeat learning intact (enforced by the strict identity guard).
Wrong=0 / no-hot-path-repair are preserved by composition — the heartbeat only proposes
(HITL untouched) + consolidates sound session memory.
"""
from __future__ import annotations
import json
from chat.always_on import CLOSURE_CEILING, LIVED_LIFE_FILENAME, run_continuous
from chat.runtime import ChatRuntime, RuntimeConfig
from core.cognition.pipeline import CognitiveTurnPipeline
from generate.meaning_graph.reader import comprehend
from generate.realize import realize_comprehension, recall_realized
from session.context import SessionContext
_RESUME_CONFIG = RuntimeConfig(
persist_session_state=True, # Shape B+ — the lived field/vault survive reboot
consolidate_determinations=True, # Step D — the loop learns from determined facts
strict_identity_continuity=True, # a reboot must be the SAME life or refuse (load guard)
)
def _stored_members(ctx: SessionContext, subject: str) -> set[str]:
"""The set of ``b`` for which ``member(subject, b)`` is a STORED realized record
(recall, not on-the-fly re-derivation)."""
return {
f.relation_arguments[1]
for f in recall_realized(ctx, subject=subject, predicate="member")
}
def _seed_continuous_life(runtime: ChatRuntime) -> None:
"""Seed a consolidatable held self — member(socrates, man) + subset(man, mortal),
from which the idle heartbeat DERIVES member(socrates, mortal) — and excite the field
so closure is observed, not vacuous."""
ctx = runtime._context
realize_comprehension(comprehend("Socrates is a man."), ctx)
realize_comprehension(comprehend("All men are mortals."), ctx)
# A real cognitive turn excites the field so ``versor_condition`` is observable.
CognitiveTurnPipeline(runtime=runtime).run("Socrates is a man.")
def test_heartbeat_lives_learns_and_converges(tmp_path) -> None:
runtime = ChatRuntime(config=_RESUME_CONFIG, engine_state_path=tmp_path / "engine_state")
_seed_continuous_life(runtime)
# The derived membership is NOT stored before the heartbeat runs.
assert "mortal" not in _stored_members(runtime._context, "socrates")
report = run_continuous(runtime, heartbeats=5)
assert report.heartbeats == 5
assert report.final_checkpoint_ok
assert report.identity # the life carries a content identity (telemetry)
# LIVES: closure holds by construction across the uptime, and it is OBSERVED (a real
# field exists), not vacuously true.
assert report.closure_observed
assert report.closure_held
observed = [r.versor_condition for r in report.records if r.versor_condition is not None]
assert observed and all(vc < CLOSURE_CEILING for vc in observed)
# LEARNS while idle: Step-D consolidation derived AND STORED member(socrates, mortal)
# during the heartbeat (non-vacuous — it was absent before run_continuous).
assert report.total_facts_consolidated >= 1
assert "mortal" in _stored_members(runtime._context, "socrates")
# CONVERGES: a saturated life stops churning — the final beat does no work.
assert report.records[-1].did_work is False
def test_life_survives_interruption_as_the_same_life(tmp_path) -> None:
state_dir = tmp_path / "engine_state"
runtime = ChatRuntime(config=_RESUME_CONFIG, engine_state_path=state_dir)
_seed_continuous_life(runtime)
report = run_continuous(runtime, heartbeats=3)
# The heartbeat's ACCUMULATED learning before the interruption.
assert report.total_facts_consolidated >= 1
assert "mortal" in _stored_members(runtime._context, "socrates")
# Interruption: the live runtime is dropped. The next runtime over the SAME engine-state
# dir is the reboot. Under strict_identity_continuity, constructing it ENFORCES the
# same-life identity guard — a clean construction means the stamped checkpoint identity
# matched the recomputed one (the SAME life, not a new one).
del runtime
resumed = ChatRuntime(config=_RESUME_CONFIG, engine_state_path=state_dir)
# The heartbeat's accumulated learning survived the reboot as a STORED record — not
# merely re-derivable from the seeds: the consolidated member(socrates, mortal) is
# recalled directly. (With consolidation off, this would NOT be stored — non-vacuous.)
assert "mortal" in _stored_members(resumed._context, "socrates")
def test_persisted_run_is_a_readable_lived_life_surface(tmp_path) -> None:
"""run_continuous(report_path=...) leaves the workbench a readable, validated surface —
the persist seam the Lived Life route reads (engine_state/lived_life.json)."""
from workbench.lived_life import lived_life_from_payload
state_dir = tmp_path / "engine_state"
runtime = ChatRuntime(config=_RESUME_CONFIG, engine_state_path=state_dir)
_seed_continuous_life(runtime)
report_path = state_dir / LIVED_LIFE_FILENAME
report = run_continuous(runtime, heartbeats=4, report_path=report_path)
# The artifact exists where the workbench reads it, and round-trips through the
# read-model honesty gate (closure_held / totals consistent with the per-beat bytes).
assert report_path.exists()
raw = json.loads(report_path.read_text(encoding="utf-8"))
surface = lived_life_from_payload(raw)
assert surface.status == "recorded"
assert surface.heartbeats == report.heartbeats == 4
assert surface.closure_observed and surface.closure_held
assert surface.total_facts_consolidated == report.total_facts_consolidated >= 1
assert surface.converged is True # the saturated life is at rest by the final beat
def test_heartbeat_never_repairs_closure(tmp_path) -> None:
"""The heartbeat READS versor_condition as evidence; it must never repair the field.
A no-op idle life (nothing to learn) keeps closure stable without any mutation."""
runtime = ChatRuntime(config=_RESUME_CONFIG, engine_state_path=tmp_path / "engine_state")
_seed_continuous_life(runtime)
assert runtime._context.state is not None
field_before = runtime._context.state.F.copy()
report = run_continuous(runtime, heartbeats=4)
# The field is byte-unchanged by the idle heartbeat (no propagation, no repair).
assert runtime._context.state is not None
assert (runtime._context.state.F == field_before).all()
assert report.closure_observed and report.closure_held