From 7ead395eebbd4752d98b704056370a7682a3463e Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 14 Jun 2026 16:45:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(workbench):=20Lived=20Life=20surface=20?= =?UTF-8?q?=E2=80=94=20the=20always-on=20heartbeat=20made=20felt=20(read-o?= =?UTF-8?q?nly,=20persist-first)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .gitignore | 1 + chat/always_on.py | 61 ++- docs/workbench/UI-UX-GUIDE.md | 4 +- tests/test_l10_always_on.py | 28 +- tests/test_workbench_lived_life.py | 239 ++++++++++++ workbench-ui/schema-snapshot.json | 28 ++ workbench-ui/src/api/client.ts | 5 + workbench-ui/src/api/queries.ts | 12 + workbench-ui/src/app/App.tsx | 6 + workbench-ui/src/app/Shell.test.tsx | 5 +- .../app/lived-life/LivedLifeRoute.test.tsx | 198 ++++++++++ .../src/app/lived-life/LivedLifeRoute.tsx | 365 ++++++++++++++++++ .../src/app/routeConformance.test.tsx | 17 + workbench-ui/src/app/routes.ts | 13 + .../CommandPalette.keyboard.test.tsx | 8 +- workbench-ui/src/types/api.ts | 48 +++ workbench/api.py | 2 + workbench/lived_life.py | 199 ++++++++++ workbench/readers.py | 31 ++ workbench/schemas.py | 61 +++ 20 files changed, 1321 insertions(+), 10 deletions(-) create mode 100644 tests/test_workbench_lived_life.py create mode 100644 workbench-ui/src/app/lived-life/LivedLifeRoute.test.tsx create mode 100644 workbench-ui/src/app/lived-life/LivedLifeRoute.tsx create mode 100644 workbench/lived_life.py diff --git a/.gitignore b/.gitignore index 3498f48d..c5f48a98 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ teaching/math_proposals/proposals.jsonl engine_state/manifest.json engine_state/recognizers.jsonl engine_state/discovery_candidates.jsonl +engine_state/lived_life.json # Private outreach / personal packet — never tracked 01_Anthropic/ diff --git a/chat/always_on.py b/chat/always_on.py index 2301f8ec..c272713c 100644 --- a/chat/always_on.py +++ b/chat/always_on.py @@ -36,14 +36,21 @@ the idle/heartbeat half. from __future__ import annotations +import json import time from dataclasses import dataclass -from typing import Callable +from pathlib import Path +from typing import Any, Callable from algebra.versor import versor_condition from core.engine_identity import engine_identity_for_config from engine_state import get_git_revision +# The name of the persisted lived-life artifact (one per always-on run) in the +# engine-state dir, read-only, for the workbench Lived Life surface. +LIVED_LIFE_FILENAME = "lived_life.json" +LIVED_LIFE_SCHEMA_VERSION = "lived_life_v1" + # The non-negotiable field invariant (CLAUDE.md). The heartbeat READS this as evidence; # it never repairs to keep it true — closure is owned by ``algebra/versor.py``. CLOSURE_CEILING = 1e-6 @@ -88,6 +95,45 @@ class AlwaysOnReport: return len(self.records) +def serialize_report(report: AlwaysOnReport) -> dict[str, Any]: + """Deterministic, JSON-able projection of an always-on run — the persisted lived-life + evidence the workbench reads.""" + return { + "schema_version": LIVED_LIFE_SCHEMA_VERSION, + "identity": report.identity, + "heartbeats": report.heartbeats, + "closure_ceiling": CLOSURE_CEILING, + "closure_observed": report.closure_observed, + "closure_held": report.closure_held, + "final_checkpoint_ok": report.final_checkpoint_ok, + "total_facts_consolidated": report.total_facts_consolidated, + "total_proposals_created": report.total_proposals_created, + "records": [ + { + "tick": r.tick, + "versor_condition": r.versor_condition, + "field_valid": r.field_valid, + "facts_consolidated": r.facts_consolidated, + "proposals_created": r.proposals_created, + "pending_proposals": r.pending_proposals, + "did_work": r.did_work, + } + for r in report.records + ], + } + + +def write_lived_life(report: AlwaysOnReport, path: Path) -> None: + """Persist the lived-life evidence deterministically (sorted keys) so the workbench can + read it as a read-only artifact. Overwrites — the artifact is the latest always-on run + (a cumulative whole-life log is a future enhancement).""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(serialize_report(report), indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + def _live_versor_condition(runtime) -> float | None: """Read the closure of the runtime's live field, or None if no turn built one yet. @@ -106,6 +152,7 @@ def run_continuous( sleep_seconds: float = 0.0, on_heartbeat: Callable[[HeartbeatRecord], None] | None = None, stop: Callable[[], bool] | None = None, + report_path: Path | None = None, ) -> AlwaysOnReport: """Run the always-on heartbeat for up to ``heartbeats`` beats. @@ -118,6 +165,13 @@ def run_continuous( Bounded for falsifiable soaks; a daemon passes a large ``heartbeats`` + a ``stop`` predicate (and a real ``sleep_seconds`` cadence). ``stop`` is checked BEFORE each beat so a clean shutdown still persists the final state. + + When ``report_path`` is given, the run's lived-life evidence is persisted there after + the loop exits — point it at ``/lived_life.json`` so the workbench Lived + Life surface reads the continuous life. A clean ``stop`` still writes the full report + (the report captures everything accumulated up to the stop). On a crash the engine + state is still checkpointed in ``finally`` for recovery, but the workbench report is + best-effort and simply not refreshed — the surface keeps the last good run. """ if heartbeats < 0: raise ValueError("heartbeats must be >= 0") @@ -161,7 +215,7 @@ def run_continuous( final_checkpoint_ok = False observed = [r.versor_condition for r in records if r.versor_condition is not None] - return AlwaysOnReport( + report = AlwaysOnReport( records=tuple(records), identity=identity, closure_observed=bool(observed), @@ -170,3 +224,6 @@ def run_continuous( total_facts_consolidated=sum(r.facts_consolidated for r in records), total_proposals_created=sum(r.proposals_created for r in records), ) + if report_path is not None: + write_lived_life(report, report_path) + return report diff --git a/docs/workbench/UI-UX-GUIDE.md b/docs/workbench/UI-UX-GUIDE.md index ac51ab63..8f252955 100644 --- a/docs/workbench/UI-UX-GUIDE.md +++ b/docs/workbench/UI-UX-GUIDE.md @@ -59,7 +59,7 @@ successful proof. ## 4. Current Route Map The route registry in `workbench-ui/src/app/routes.ts` is the source of truth. -Current route count: 15. +Current route count: 16. | Section | Route | Path | Shortcut | Purpose | |---|---|---|---|---| @@ -71,6 +71,7 @@ Current route count: 15. | Determinism | Demos | `/demos` | Palette | Run registered determinism demos. | | Evidence | Proposals | `/proposals` | `⌘4` | Review cognition and math proposal evidence. | | Evidence | Runs | `/runs` | `⌘6` | Browse recorded run/session evidence. | +| Evidence | Lived Life | `/lived-life` | Palette | Watch the always-on heartbeat hold one continuous life (closure read-not-repaired over uptime + learned-while-idle). | | Evidence | Vault | `/vault` | `⌘8` | Inspect persisted vault metadata when persistence exists. | | Evidence | Audit | `/audit` | `⌘9` | Read deterministic audit events. | | Discipline | Evals | `/evals` | `⌘5` | Run/read allowlisted eval lanes and wrong=0 ledgers. | @@ -93,6 +94,7 @@ the command palette. | Demos | Registered demo scenarios pass or fail with recorded scenario evidence and proof/entailment DAGs where the demo emits them. | | Proposals | Proposal evidence, replay facts, and ratification commands are inspectable without applying mutation. | | Runs | Recorded run/session references, checkpoint gaps, and identity-continuity verdicts are discoverable. | +| Lived Life | A persisted always-on run shows the engine living + learning over uptime, with closure (`versor_condition < 1e-6`) read as evidence each beat (never repaired); the surface's `closure_held` is consistency-checked against the per-beat measurements, so it can never paint a breached field as valid. | | Vault | Persisted vault metadata is inspectable when persistence is configured. | | Audit | Audit events are readable with payload digests and mutation-boundary flags. | | Evals | Allowlisted eval lanes and wrong/correct/refused metrics are visible. | diff --git a/tests/test_l10_always_on.py b/tests/test_l10_always_on.py index ff8b114e..88692b66 100644 --- a/tests/test_l10_always_on.py +++ b/tests/test_l10_always_on.py @@ -11,7 +11,9 @@ Wrong=0 / no-hot-path-repair are preserved by composition — the heartbeat only from __future__ import annotations -from chat.always_on import CLOSURE_CEILING, run_continuous +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 @@ -93,6 +95,30 @@ def test_life_survives_interruption_as_the_same_life(tmp_path) -> None: 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.""" diff --git a/tests/test_workbench_lived_life.py b/tests/test_workbench_lived_life.py new file mode 100644 index 00000000..a0617e50 --- /dev/null +++ b/tests/test_workbench_lived_life.py @@ -0,0 +1,239 @@ +"""Lived Life surface — the read-only projection of a persisted always-on run. + +These prove the surface is HONEST BY CONSTRUCTION: an absent artifact is honest absence, +a recorded artifact projects faithfully, and a TAMPERED artifact (a beat that lies about +its closure, an inflated aggregate, a miscounted total) makes ``validate`` raise rather +than render a false continuity claim. That fail-loud-on-tamper property is the wrong=0 +analogue for the continuity surface (CLAUDE.md Schema-Defined Proof Obligations): each +mutation below would silently pass if the gate were decoration. +""" + +from __future__ import annotations + +import pytest + +from chat.always_on import ( + AlwaysOnReport, + HeartbeatRecord, + serialize_report, + write_lived_life, +) +from workbench import readers +from workbench.lived_life import ( + lived_life_from_payload, + missing_lived_life, + validate, +) +from workbench.schemas import LivedLife, LivedLifeHeartbeat + + +def _report() -> AlwaysOnReport: + """A representative life: closure observed + held, learns on beat 0, at rest on beat 1.""" + records = ( + HeartbeatRecord( + tick=0, + versor_condition=8.2e-13, + field_valid=True, + facts_consolidated=1, + proposals_created=0, + pending_proposals=0, + did_work=True, + ), + HeartbeatRecord( + tick=1, + versor_condition=8.2e-13, + field_valid=True, + facts_consolidated=0, + proposals_created=0, + pending_proposals=0, + did_work=False, + ), + ) + return AlwaysOnReport( + records=records, + identity="sha256:lived-life-test", + closure_observed=True, + closure_held=True, + final_checkpoint_ok=True, + total_facts_consolidated=1, + total_proposals_created=0, + ) + + +def test_missing_artifact_is_honest_absence() -> None: + surface = missing_lived_life("no always-on run has been persisted yet") + assert surface.status == "missing_evidence" + assert surface.identity is None + assert surface.heartbeats == 0 + # An absent life claims nothing. + assert surface.closure_observed is False and surface.closure_held is False + assert surface.records == [] + + +def test_recorded_artifact_projects_faithfully() -> None: + surface = lived_life_from_payload(serialize_report(_report())) + assert surface.status == "recorded" + assert surface.identity == "sha256:lived-life-test" + assert surface.heartbeats == 2 + assert surface.closure_observed and surface.closure_held + assert surface.total_facts_consolidated == 1 + # CONVERGED is derived from the records (final beat at rest), not trusted from bytes. + assert surface.converged is True + assert [b.tick for b in surface.records] == [0, 1] + # No current identity supplied -> the resume verdict is honestly unknown. + assert surface.resume_status == "unknown" + + +def test_resume_verdict_tracks_identity_vs_substrate() -> None: + payload = serialize_report(_report()) # identity == "sha256:lived-life-test" + # Matching substrate identity -> a reboot resumes THIS life. + same = lived_life_from_payload(payload, current_identity="sha256:lived-life-test") + assert same.resume_status == "would_resume" + assert "resumes this life" in same.resume_summary + # Changed substrate identity -> a reboot would refuse (IdentityContinuityError). + changed = lived_life_from_payload(payload, current_identity="sha256:other") + assert changed.resume_status == "substrate_changed" + assert "refuse" in changed.resume_summary + # No recomputable substrate identity -> honest unknown. + unknown = lived_life_from_payload(payload, current_identity=None) + assert unknown.resume_status == "unknown" + + +def test_reader_reads_persisted_artifact(tmp_path, monkeypatch) -> None: + state_dir = tmp_path / "engine_state" + state_dir.mkdir() + write_lived_life(_report(), state_dir / "lived_life.json") + monkeypatch.setattr(readers, "ENGINE_STATE_ROOT", state_dir) + + surface = readers.lived_life() + assert surface.status == "recorded" + assert surface.closure_held and surface.converged + # The surface links to the raw artifact for provenance. + assert surface.artifact is not None + assert surface.artifact.kind == "lived_life" + + +def test_reader_absent_artifact(tmp_path, monkeypatch) -> None: + monkeypatch.setattr(readers, "ENGINE_STATE_ROOT", tmp_path / "engine_state") + surface = readers.lived_life() + assert surface.status == "missing_evidence" + + +def test_unrecognized_schema_is_absence_not_error() -> None: + payload = serialize_report(_report()) + payload["schema_version"] = "lived_life_v999" + surface = lived_life_from_payload(payload) + assert surface.status == "missing_evidence" + assert "schema_version" in (surface.missing_reason or "") + + +# --- Non-vacuous tamper rejections: each mutation MUST make validate raise. --- + + +def test_tamper_field_valid_lies_about_breach_is_rejected() -> None: + payload = serialize_report(_report()) + # A beat breaches the ceiling but still claims a valid field. + payload["records"][0]["versor_condition"] = 5e-3 + payload["records"][0]["field_valid"] = True + with pytest.raises(ValueError, match="field_valid"): + lived_life_from_payload(payload) + + +def test_tamper_inflated_closure_held_is_rejected() -> None: + payload = serialize_report(_report()) + # An honest beat breaches the ceiling, but the aggregate lies that closure held. + payload["records"][0]["versor_condition"] = 5e-3 + payload["records"][0]["field_valid"] = False + payload["closure_held"] = True + with pytest.raises(ValueError, match="closure_held"): + lived_life_from_payload(payload) + + +def test_tamper_closure_observed_is_rejected() -> None: + payload = serialize_report(_report()) + # No beat observed a field, but the surface claims one did. + for rec in payload["records"]: + rec["versor_condition"] = None + rec["field_valid"] = True + payload["closure_observed"] = True + payload["closure_held"] = True + with pytest.raises(ValueError, match="closure_observed"): + lived_life_from_payload(payload) + + +def test_tamper_miscounted_total_is_rejected() -> None: + payload = serialize_report(_report()) + payload["total_facts_consolidated"] = 99 # records sum to 1 + with pytest.raises(ValueError, match="total_facts_consolidated"): + lived_life_from_payload(payload) + + +def test_tamper_converged_mismatch_is_rejected() -> None: + # Direct construction: a recorded surface whose `converged` contradicts the final beat. + beats = [ + LivedLifeHeartbeat( + tick=0, + versor_condition=8.2e-13, + field_valid=True, + facts_consolidated=0, + proposals_created=0, + pending_proposals=0, + did_work=True, # final beat still working -> NOT converged + ) + ] + surface = LivedLife( + schema_version="lived_life_v1", + status="recorded", + missing_reason=None, + identity="sha256:x", + heartbeats=1, + closure_observed=True, + closure_held=True, + closure_ceiling=1e-6, + final_checkpoint_ok=True, + converged=True, # lies: the final beat did work + total_facts_consolidated=0, + total_proposals_created=0, + current_identity="sha256:x", + resume_status="would_resume", + resume_summary="a reboot resumes this life — its identity matches the current substrate", + records=beats, + ) + with pytest.raises(ValueError, match="converged"): + validate(surface) + + +def test_tamper_resume_status_mismatch_is_rejected() -> None: + # Direct construction: a surface that claims "would_resume" while the persisted identity + # differs from the current substrate identity — a reboot would actually refuse. + surface = LivedLife( + schema_version="lived_life_v1", + status="recorded", + missing_reason=None, + identity="sha256:persisted", + heartbeats=0, + closure_observed=False, + closure_held=True, # vacuously true with no observed field (passes the closure gate) + closure_ceiling=1e-6, + final_checkpoint_ok=True, + converged=False, + total_facts_consolidated=0, + total_proposals_created=0, + current_identity="sha256:current-DIFFERENT", + resume_status="would_resume", # lies: identities differ -> substrate_changed + resume_summary="a reboot resumes this life — its identity matches the current substrate", + records=[], + ) + with pytest.raises(ValueError, match="resume_status"): + validate(surface) + + +def test_persist_is_deterministic() -> None: + report = _report() + once = serialize_report(report) + twice = serialize_report(report) + assert once == twice + # And the written bytes are stable (sorted keys), so the artifact is replay-citable. + import json + + assert json.dumps(once, sort_keys=True) == json.dumps(twice, sort_keys=True) diff --git a/workbench-ui/schema-snapshot.json b/workbench-ui/schema-snapshot.json index bffc27b4..e2a6b580 100644 --- a/workbench-ui/schema-snapshot.json +++ b/workbench-ui/schema-snapshot.json @@ -237,6 +237,34 @@ "source_digest", "calibration_evidence_ref" ], + "LivedLife": [ + "schema_version", + "status", + "missing_reason", + "identity", + "heartbeats", + "closure_observed", + "closure_held", + "closure_ceiling", + "final_checkpoint_ok", + "converged", + "total_facts_consolidated", + "total_proposals_created", + "current_identity", + "resume_status", + "resume_summary", + "records", + "artifact" + ], + "LivedLifeHeartbeat": [ + "tick", + "versor_condition", + "field_valid", + "facts_consolidated", + "proposals_created", + "pending_proposals", + "did_work" + ], "LogosAlignmentRow": [ "edge_id", "source_id", diff --git a/workbench-ui/src/api/client.ts b/workbench-ui/src/api/client.ts index 78489df8..a1240744 100644 --- a/workbench-ui/src/api/client.ts +++ b/workbench-ui/src/api/client.ts @@ -30,6 +30,7 @@ import type { FieldEvidence, EvidenceBundle, DeterminismTour, + LivedLife, TurnJournalEntry, TurnJournalSummary, ContemplationRunDetail, @@ -210,6 +211,10 @@ export async function fetchTour(): Promise { return apiFetch("/tour"); } +export async function fetchLivedLife(): Promise { + return apiFetch("/lived-life"); +} + export async function fetchAuditEvents( limit?: number, offset?: number, diff --git a/workbench-ui/src/api/queries.ts b/workbench-ui/src/api/queries.ts index fc57fa97..7c13178d 100644 --- a/workbench-ui/src/api/queries.ts +++ b/workbench-ui/src/api/queries.ts @@ -34,6 +34,7 @@ import { fetchTraceField, fetchTraceBundle, fetchTour, + fetchLivedLife, fetchTraceTurns, fetchContemplationRun, fetchContemplationRuns, @@ -71,6 +72,7 @@ import type { FieldEvidence, EvidenceBundle, DeterminismTour, + LivedLife, TurnJournalEntry, TurnJournalSummary, EvalLaneSummary, @@ -255,6 +257,16 @@ export function useTour() { }); } +export function useLivedLife() { + return useQuery({ + queryKey: ["api", "lived-life"], + queryFn: fetchLivedLife, + // The life advances over uptime — keep it fresh without hammering. + refetchInterval: 30_000, + refetchOnWindowFocus: false, + }); +} + export function useAuditEvents(limit?: number, offset?: number) { return useQuery<{ items: AuditEvent[] }, WorkbenchApiError>({ queryKey: ["api", "audit", "events", limit ?? null, offset ?? null], diff --git a/workbench-ui/src/app/App.tsx b/workbench-ui/src/app/App.tsx index 19b4250f..48b8f6e8 100644 --- a/workbench-ui/src/app/App.tsx +++ b/workbench-ui/src/app/App.tsx @@ -37,6 +37,11 @@ const ProposalsRoute = lazy(() => const RunsRoute = lazy(() => import("./runs/RunsRoute").then((module) => ({ default: module.RunsRoute })), ); +const LivedLifeRoute = lazy(() => + import("./lived-life/LivedLifeRoute").then((module) => ({ + default: module.LivedLifeRoute, + })), +); const VaultRoute = lazy(() => import("./vault/VaultRoute").then((module) => ({ default: module.VaultRoute })), ); @@ -87,6 +92,7 @@ export const ROUTE_ELEMENTS: RouteElementMap = { demos: lazyRoute(), proposals: lazyRoute(), runs: lazyRoute(), + "lived-life": lazyRoute(), vault: lazyRoute(), audit: lazyRoute(), evals: lazyRoute(), diff --git a/workbench-ui/src/app/Shell.test.tsx b/workbench-ui/src/app/Shell.test.tsx index 07f19fdd..16552cdb 100644 --- a/workbench-ui/src/app/Shell.test.tsx +++ b/workbench-ui/src/app/Shell.test.tsx @@ -85,11 +85,11 @@ describe("Shell", () => { expect(document.querySelector('[data-density="compact"]')).toBeInTheDocument(); }); - it("LeftNav has exactly 15 items in section-grouped order", () => { + it("LeftNav has exactly 16 items in section-grouped order", () => { renderShell(); const nav = document.querySelector('[data-region="leftnav"]')!; const links = nav.querySelectorAll("a"); - expect(links).toHaveLength(15); + expect(links).toHaveLength(16); const labels = Array.from(links).map((l) => l.textContent); // Grouped by section (Converse → Cognition → Determinism → Evidence → // Discipline → Substrate → Settings), derived from the route registry. @@ -102,6 +102,7 @@ describe("Shell", () => { "Demos", "Proposals", "Runs", + "Lived Life", "Vault", "Audit", "Evals", diff --git a/workbench-ui/src/app/lived-life/LivedLifeRoute.test.tsx b/workbench-ui/src/app/lived-life/LivedLifeRoute.test.tsx new file mode 100644 index 00000000..911e5741 --- /dev/null +++ b/workbench-ui/src/app/lived-life/LivedLifeRoute.test.tsx @@ -0,0 +1,198 @@ +import { render, screen } from "@testing-library/react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { MemoryRouter } from "react-router-dom"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createTestQueryClient } from "../../test/createTestQueryClient"; +import { LivedLifeRoute, LIVED_LIFE_ABSENCE_STATEMENT } from "./LivedLifeRoute"; +import type { LivedLife } from "../../types/api"; + +/** + * The Lived Life surface must render the continuous life HONESTLY: it shows the + * heartbeat-over-uptime facts when a life is recorded, and it can NEVER paint a + * breached field as valid. These assert the rendered content, not just that the + * route mounts (the frontend analogue of the backend's non-vacuous tamper tests). + */ + +const GENERATED_AT = "2026-06-14T00:00:00Z"; + +function stubFetch(life: LivedLife) { + vi.stubGlobal( + "fetch", + vi.fn(() => + Promise.resolve({ + json: async () => ({ ok: true, generated_at: GENERATED_AT, data: life }), + }), + ), + ); +} + +function renderRoute() { + return render( + + + + + , + ); +} + +const HEALTHY: LivedLife = { + schema_version: "lived_life_v1", + status: "recorded", + missing_reason: null, + identity: "sha256:abcdef0123456789", + heartbeats: 3, + closure_observed: true, + closure_held: true, + closure_ceiling: 1e-6, + final_checkpoint_ok: true, + converged: true, + total_facts_consolidated: 2, + total_proposals_created: 0, + current_identity: "sha256:abcdef0123456789", // matches identity -> would_resume + resume_status: "would_resume", + resume_summary: + "a reboot resumes this life — its identity matches the current substrate", + records: [ + { + tick: 0, + versor_condition: 8.2e-13, + field_valid: true, + facts_consolidated: 2, + proposals_created: 0, + pending_proposals: 0, + did_work: true, + }, + { + tick: 1, + versor_condition: 8.2e-13, + field_valid: true, + facts_consolidated: 0, + proposals_created: 0, + pending_proposals: 0, + did_work: false, + }, + { + tick: 2, + versor_condition: 8.2e-13, + field_valid: true, + facts_consolidated: 0, + proposals_created: 0, + pending_proposals: 0, + did_work: false, + }, + ], + artifact: { + artifact_id: "engine_state/lived_life.json", + kind: "lived_life", + path: "engine_state/lived_life.json", + digest: "sha256:deadbeefcafe", + created_at: null, + }, +}; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("LivedLifeRoute", () => { + it("renders the continuous life: heartbeats, closure held, learned-while-idle, beats", async () => { + stubFetch(HEALTHY); + renderRoute(); + + expect(await screen.findByText("One continuous life")).toBeInTheDocument(); + // Learned while idle (non-zero, from the records). + expect(screen.getByText("2 learned while idle")).toBeInTheDocument(); + // Closure held below the ceiling (never claimed without observation). + expect(screen.getAllByText(/closure held/i).length).toBeGreaterThan(0); + // The heartbeat over uptime: each beat is shown. + expect(screen.getByText("beat 0")).toBeInTheDocument(); + expect(screen.getByText("beat 2")).toBeInTheDocument(); + // A saturated life is at rest. + expect(screen.getAllByText(/at rest/i).length).toBeGreaterThan(0); + // The resume-as-same-life verdict is made felt: this life wakes up as itself. + expect(screen.getByText(/resumes as same life/i)).toBeInTheDocument(); + expect( + screen.getByText(/its identity matches the current substrate/i), + ).toBeInTheDocument(); + // The per-reboot lineage chain is honestly cross-linked to Runs. + expect(screen.getByRole("link", { name: /Runs/ })).toBeInTheDocument(); + }); + + it("a breached beat surfaces the closure warning, never a false 'held'", async () => { + const breached: LivedLife = { + ...HEALTHY, + heartbeats: 1, + closure_held: false, + converged: true, + records: [ + { + tick: 0, + versor_condition: 5e-3, // breaches the 1e-6 ceiling + field_valid: false, + facts_consolidated: 0, + proposals_created: 0, + pending_proposals: 0, + did_work: false, + }, + ], + }; + stubFetch(breached); + renderRoute(); + + expect(await screen.findByText(/closure BREACHED/i)).toBeInTheDocument(); + // The breached beat is marked INVALID, never painted valid. + expect(screen.getByText("INVALID")).toBeInTheDocument(); + expect(screen.queryByText(/closure held/i)).not.toBeInTheDocument(); + }); + + it("a changed substrate surfaces the would-refuse warning, never a false resume", async () => { + const changed: LivedLife = { + ...HEALTHY, + current_identity: "sha256:0000different0000", + resume_status: "substrate_changed", + resume_summary: + "the substrate changed — a reboot would refuse (IdentityContinuityError)", + }; + stubFetch(changed); + renderRoute(); + + expect( + (await screen.findAllByText(/substrate changed/i)).length, + ).toBeGreaterThan(0); + expect( + screen.getByText(/a reboot would refuse/i), + ).toBeInTheDocument(); + expect(screen.queryByText(/resumes as same life/i)).not.toBeInTheDocument(); + }); + + it("an absent artifact shows honest absence, never a fabricated life", async () => { + const absent: LivedLife = { + ...HEALTHY, + status: "missing_evidence", + missing_reason: "no always-on run has been persisted yet", + identity: null, + heartbeats: 0, + closure_observed: false, + closure_held: false, + converged: false, + total_facts_consolidated: 0, + total_proposals_created: 0, + current_identity: null, + resume_status: "unknown", + resume_summary: + "resume verdict unavailable — the current substrate identity could not be recomputed", + records: [], + artifact: null, + }; + stubFetch(absent); + renderRoute(); + + expect( + await screen.findByText(LIVED_LIFE_ABSENCE_STATEMENT), + ).toBeInTheDocument(); + // No fabricated heartbeat content. + expect(screen.queryByText("One continuous life")).not.toBeInTheDocument(); + expect(screen.queryByText("beat 0")).not.toBeInTheDocument(); + }); +}); diff --git a/workbench-ui/src/app/lived-life/LivedLifeRoute.tsx b/workbench-ui/src/app/lived-life/LivedLifeRoute.tsx new file mode 100644 index 00000000..909edf77 --- /dev/null +++ b/workbench-ui/src/app/lived-life/LivedLifeRoute.tsx @@ -0,0 +1,365 @@ +import { useState, type ReactNode } from "react"; +import { Activity, HeartPulse } from "lucide-react"; +import { Link } from "react-router-dom"; +import { WorkbenchApiError } from "../../api/client"; +import { useLivedLife } from "../../api/queries"; +import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge"; +import { MetadataTable } from "../../design/components/MetadataTable/MetadataTable"; +import { Panel } from "../../design/components/Panel/Panel"; +import { StableJsonViewer } from "../../design/components/StableJsonViewer"; +import { Button } from "../../design/components/primitives/Button"; +import { EmptyState } from "../../design/components/states/EmptyState"; +import { ErrorState } from "../../design/components/states/ErrorState"; +import { LoadingState } from "../../design/components/states/LoadingState"; +import type { LivedLife, LivedLifeHeartbeat } from "../../types/api"; + +// Conformance contract strings (ADR-0162 §6) — kept verbatim in +// routeConformance.test.tsx; the absence state IS the primary state until an +// always-on run lands an artifact (fail-closed, like Vault/Calibration). +export const LIVED_LIFE_LOADING = "Loading lived life..."; +export const LIVED_LIFE_ABSENCE_STATEMENT = + "No always-on run recorded yet. The continuous-life heartbeat persists its evidence here when it runs."; +export const LIVED_LIFE_ABSENCE_ACTION = + "Run the always-on heartbeat to persist engine_state/lived_life.json"; + +function errorMessage(error: unknown): string { + return error instanceof WorkbenchApiError + ? error.message + : "Lived-life request failed."; +} + +function digestPayload(value: string | null | undefined): string | null { + if (!value) return null; + return value.replace(/^sha256:/, ""); +} + +function closureDisplay(versorCondition: number | null): string { + return typeof versorCondition === "number" + ? versorCondition.toExponential(3) + : "no field yet"; +} + +/** A small status pill. `tone` maps to a state color token. */ +function Pill({ + tone, + children, +}: { + tone: "verified" | "warning" | "muted"; + children: ReactNode; +}) { + const color = + tone === "verified" + ? "var(--color-state-verified)" + : tone === "warning" + ? "var(--color-state-warning-text)" + : "var(--color-text-secondary)"; + return ( + + {children} + + ); +} + +/** The headline: one continuous life, stated as falsifiable facts. */ +function LifeHeadline({ life }: { life: LivedLife }) { + const closureTone = life.closure_held ? "verified" : "warning"; + return ( + + + always-on heartbeat + + } + > +
+

+ CORE held itself alive over {life.heartbeats}{" "} + {life.heartbeats === 1 ? "heartbeat" : "heartbeats"} with no user turn, + learned while idle, and kept its field valid by construction — the + heartbeat READS closure as evidence, it never repairs it. +

+
+ + {life.heartbeats}{" "} + {life.heartbeats === 1 ? "heartbeat" : "heartbeats"} + + + {life.closure_observed + ? life.closure_held + ? `closure held < ${life.closure_ceiling.toExponential(0)}` + : "closure BREACHED" + : "no field observed"} + + 0 ? "verified" : "muted"}> + {life.total_facts_consolidated} learned while idle + + {life.total_proposals_created > 0 ? ( + + {life.total_proposals_created} proposals (reviewable) + + ) : null} + + {life.converged ? "at rest" : "still churning"} + + + {life.resume_status === "would_resume" + ? "resumes as same life" + : life.resume_status === "substrate_changed" + ? "substrate changed" + : "resume unknown"} + +
+
+
+ ); +} + +function resumeTone( + status: LivedLife["resume_status"], +): "verified" | "warning" | "muted" { + if (status === "would_resume") return "verified"; + if (status === "substrate_changed") return "warning"; + return "muted"; +} + +function LifeSummary({ life }: { life: LivedLife }) { + const identity = digestPayload(life.identity); + const current = digestPayload(life.current_identity); + const artifact = digestPayload(life.artifact?.digest); + return ( + + ) : ( + not recorded + ), + }, + { key: "heartbeats", value: String(life.heartbeats), mono: true }, + { + key: "closure_observed", + value: life.closure_observed ? "yes" : "no field this run", + mono: true, + }, + { + key: "closure_held", + value: life.closure_held + ? `held (< ${life.closure_ceiling.toExponential(0)})` + : "BREACHED", + mono: true, + }, + { + key: "facts_consolidated", + value: String(life.total_facts_consolidated), + mono: true, + }, + { + key: "proposals_created", + value: String(life.total_proposals_created), + mono: true, + }, + { + key: "converged", + value: life.converged ? "yes (final beat at rest)" : "no (still working)", + mono: true, + }, + { + key: "final_checkpoint_ok", + value: life.final_checkpoint_ok ? "yes" : "FAILED", + mono: true, + }, + { + key: "current_identity", + value: current ? ( + + ) : ( + not recomputed + ), + }, + { + key: "artifact", + value: artifact ? ( + + ) : ( + not linked + ), + }, + ]} + /> + ); +} + +function HeartbeatRow({ beat }: { beat: LivedLifeHeartbeat }) { + const valid = beat.field_valid; + return ( +
+ + beat {beat.tick} + + + + {closureDisplay(beat.versor_condition)} + + + {valid ? "valid" : "INVALID"} + + + + {beat.did_work ? ( + + + {beat.facts_consolidated > 0 + ? `+${beat.facts_consolidated} fact${beat.facts_consolidated === 1 ? "" : "s"}` + : null} + {beat.proposals_created > 0 + ? ` +${beat.proposals_created} prop${beat.proposals_created === 1 ? "" : "s"}` + : null} + {beat.facts_consolidated === 0 && beat.proposals_created === 0 + ? "working" + : null} + + ) : ( + at rest + )} + +
+ ); +} + +function HeartbeatTimeline({ life }: { life: LivedLife }) { + if (life.records.length === 0) { + return ( +

+ This run recorded no heartbeats. +

+ ); + } + return ( +
+

+ Each beat advances continuous learning, then reads the field's closure + (versor_condition) as evidence. Closure + stays flat below the ceiling across the life — the engine never patches the + field to keep it valid. +

+
+ {life.records.map((beat) => ( + + ))} +
+
+ ); +} + +function ResumeVerdict({ life }: { life: LivedLife }) { + const tone = resumeTone(life.resume_status); + const color = + tone === "verified" + ? "var(--color-state-verified)" + : tone === "warning" + ? "var(--color-state-warning-text)" + : "var(--color-text-muted)"; + return ( +
+

+ {life.resume_summary} +

+

+ This is the resume guarantee made felt: a reboot recomputes the engine + identity and refuses if it differs from the persisted one, so this life + wakes up as itself, not a copy. The per-reboot lineage chain + lives under{" "} + + Runs › Identity + + . +

+
+ ); +} + +function RawJson({ life }: { life: LivedLife }) { + const [expanded, setExpanded] = useState(false); + return expanded ? ( + + ) : ( +
+

+ Raw lived-life JSON is collapsed by default. +

+ +
+ ); +} + +export function LivedLifeRoute() { + const query = useLivedLife(); + + if (query.isLoading) { + return ; + } + + if (query.isError) { + return ( + + ); + } + + const life = query.data; + if (!life || life.status !== "recorded") { + return ( + + ); + } + + return ( +
+
+ + +
+ + +
+
+ + + + + + +
+
+ ); +} diff --git a/workbench-ui/src/app/routeConformance.test.tsx b/workbench-ui/src/app/routeConformance.test.tsx index ed371721..2391d610 100644 --- a/workbench-ui/src/app/routeConformance.test.tsx +++ b/workbench-ui/src/app/routeConformance.test.tsx @@ -15,6 +15,12 @@ import { EvalsRoute } from "./evals/EvalsRoute"; import { ReplayRoute } from "./replay/ReplayRoute"; import { DemoTheaterRoute } from "./demos/DemoTheaterRoute"; import { RunsRoute } from "./runs/RunsRoute"; +import { + LivedLifeRoute, + LIVED_LIFE_LOADING, + LIVED_LIFE_ABSENCE_STATEMENT, + LIVED_LIFE_ABSENCE_ACTION, +} from "./lived-life/LivedLifeRoute"; import { PacksRoute } from "./packs/PacksRoute"; import { LogosRoute } from "./logos/LogosRoute"; import { VaultRoute } from "./vault/VaultRoute"; @@ -168,6 +174,17 @@ const MOUNT_ROUTES: MountRouteSpec[] = [ emptyStatement: "No runs recorded yet. Use Chat to create evidence.", emptyCommand: "core chat", }, + { + name: "Lived Life", + element: , + path: "/lived-life", + initialEntry: "/lived-life", + loadingLabel: LIVED_LIFE_LOADING, + // Fail-closed: until an always-on run persists an artifact, honest absence + // is the primary state (no fabricated life). + emptyStatement: LIVED_LIFE_ABSENCE_STATEMENT, + emptyCommand: LIVED_LIFE_ABSENCE_ACTION, + }, { name: "Replay", element: , diff --git a/workbench-ui/src/app/routes.ts b/workbench-ui/src/app/routes.ts index 7febf5be..4c72f098 100644 --- a/workbench-ui/src/app/routes.ts +++ b/workbench-ui/src/app/routes.ts @@ -170,6 +170,19 @@ export const WORKBENCH_ROUTES: readonly WorkbenchRoute[] = [ keyboardDigit: "6", routeConformanceRequired: true, }, + { + id: "lived-life", + path: "/lived-life", + routePattern: "lived-life", + label: "Lived Life", + description: "Watch the always-on heartbeat hold one continuous life.", + section: "Evidence", + leftNavVisible: true, + commandPaletteVisible: true, + landingRouteAllowed: true, + keyboardDigit: null, + routeConformanceRequired: true, + }, { id: "vault", path: "/vault", diff --git a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx index 4dea19fe..f45d43e8 100644 --- a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx +++ b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx @@ -52,11 +52,11 @@ describe("CommandPalette keyboard contract", () => { expect(screen.getByRole("button", { name: "Open Trace" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Open Replay" })).toBeInTheDocument(); - // One Navigate command per palette-visible route (15), derived from the - // route registry — Demos, Calibration, Contemplation, Tour, and CORE-Logos - // are included (the prior hand-maintained list of 10 dropped them). + // One Navigate command per palette-visible route (16), derived from the + // route registry — Demos, Calibration, Contemplation, Tour, CORE-Logos, and + // Lived Life are included (the prior hand-maintained list of 10 dropped them). const items = dialog.querySelectorAll('[role="option"]'); - expect(items.length).toBe(15); + expect(items.length).toBe(16); const lastIndex = items.length - 1; // Initially first item (index 0) is focused — check aria-selected diff --git a/workbench-ui/src/types/api.ts b/workbench-ui/src/types/api.ts index ae5bccdf..82c19293 100644 --- a/workbench-ui/src/types/api.ts +++ b/workbench-ui/src/types/api.ts @@ -114,6 +114,53 @@ export interface FieldEvidence { transition_inner_product: number | null; } +/** + * One beat of the continuous life (read-only telemetry mirrored from + * `chat.always_on.HeartbeatRecord`): the closure of the live field that beat + * (`versor_condition`, READ never repaired), whether it held the `< ceiling` + * invariant, and what the life learned that beat. + */ +export interface LivedLifeHeartbeat { + tick: number; + versor_condition: number | null; + field_valid: boolean; + facts_consolidated: number; + proposals_created: number; + pending_proposals: number; + did_work: boolean; +} + +/** + * L10 lived-life surface — evidence that CORE is ONE continuous life. A read-only + * projection of the persisted always-on run (`engine_state/lived_life.json`): the engine + * holds itself alive over uptime with no user turn, learns while idle (Step-D + * consolidation + proposal-only proposals), and holds closure BY CONSTRUCTION + * (`versor_condition` is READ as evidence each beat, never repaired). `closure_held` is + * consistency-checked against the per-beat measurements at construction (the wrong=0 + * analogue for the continuity surface); `converged` is honest telemetry (a saturated life + * stops churning — the final beat did no work); `identity` is the life's content identity + * (the "same life" thread, not a continuity proof — that is owned by Runs). + */ +export interface LivedLife { + schema_version: "lived_life_v1"; + status: PipelineEvidenceStatus; + missing_reason: string | null; + identity: string | null; + heartbeats: number; + closure_observed: boolean; + closure_held: boolean; + closure_ceiling: number; + final_checkpoint_ok: boolean; + converged: boolean; + total_facts_consolidated: number; + total_proposals_created: number; + current_identity: string | null; + resume_status: "would_resume" | "substrate_changed" | "unknown"; + resume_summary: string; + records: LivedLifeHeartbeat[]; + artifact: ArtifactRef | null; +} + /** * D3 shareable evidence bundle — a turn's deterministic evidence exported as one * content-addressed, citable artifact. `bundle_digest` content-addresses the @@ -452,6 +499,7 @@ export type ArtifactKind = | "contemplation_report" | "telemetry" | "engine_state_manifest" + | "lived_life" | "unknown"; export type ContentType = "json" | "jsonl" | "text" | "unknown"; diff --git a/workbench/api.py b/workbench/api.py index 70f5b505..1edd1e2b 100644 --- a/workbench/api.py +++ b/workbench/api.py @@ -149,6 +149,8 @@ class WorkbenchApi: return ApiResponse(200, ok({"status": "ok"})) if method == "GET" and path == "/runtime/status": return ApiResponse(200, ok(readers.runtime_status())) + if method == "GET" and path == "/lived-life": + return ApiResponse(200, ok(readers.lived_life())) if method == "GET" and path == "/artifacts": limit = int(query.get("limit", ["100"])[0]) return ApiResponse(200, ok({"items": readers.list_artifacts(limit=limit)})) diff --git a/workbench/lived_life.py b/workbench/lived_life.py new file mode 100644 index 00000000..a248a3d1 --- /dev/null +++ b/workbench/lived_life.py @@ -0,0 +1,199 @@ +"""L10 lived-life surface — read-only projection of the persisted always-on run. + +The always-on heartbeat (``chat.always_on``) holds CORE alive over uptime and persists +its evidence to ``engine_state/lived_life.json`` (``write_lived_life``). This module +projects that artifact into the read-only :class:`~workbench.schemas.LivedLife` model the +workbench renders, and fail-closes: a ``recorded`` surface must be CONSISTENT with the +persisted per-beat measurements — it can never claim the field stayed valid while a beat +breached the ``versor_condition`` ceiling (the wrong=0 analogue for the continuity +surface). + +No engine math is re-implemented here. The scalars (``versor_condition``, the closure +ceiling, the learning counts) are exactly what ``chat.always_on`` measured and persisted; +this module only re-projects and re-checks them. +""" + +from __future__ import annotations + +from typing import Any, Literal + +from chat.always_on import CLOSURE_CEILING, LIVED_LIFE_SCHEMA_VERSION +from workbench.schemas import ArtifactRef, LivedLife, LivedLifeHeartbeat + +ResumeStatus = Literal["would_resume", "substrate_changed", "unknown"] + +_RESUME_SUMMARY: dict[ResumeStatus, str] = { + "would_resume": "a reboot resumes this life — its identity matches the current substrate", + "substrate_changed": "the substrate changed — a reboot would refuse (IdentityContinuityError)", + "unknown": "resume verdict unavailable — the current substrate identity could not be recomputed", +} + + +def _resume_status(identity: str | None, current_identity: str | None) -> ResumeStatus: + """The L11 reboot guarantee as a self-contained verdict: a reboot recomputes the engine + identity and refuses if it differs from the persisted one, so ``would_resume`` ⟺ the + persisted life's identity equals the current substrate identity.""" + if not identity or not current_identity: + return "unknown" + return "would_resume" if identity == current_identity else "substrate_changed" + + +def missing_lived_life(reason: str) -> LivedLife: + """Honest absence — no always-on run has been persisted yet.""" + + return LivedLife( + schema_version="lived_life_v1", + status="missing_evidence", + missing_reason=reason, + identity=None, + heartbeats=0, + closure_observed=False, + closure_held=False, + closure_ceiling=CLOSURE_CEILING, + final_checkpoint_ok=False, + converged=False, + total_facts_consolidated=0, + total_proposals_created=0, + current_identity=None, + resume_status="unknown", + resume_summary=_RESUME_SUMMARY["unknown"], + records=[], + artifact=None, + ) + + +def validate(record: LivedLife) -> None: + """Fail-closed honesty gate for a recorded lived-life surface. + + Every claim the surface makes must agree EXACTLY with the per-beat measurements it is + built from, so the continuity card can never overstate the life: + + * each beat's ``field_valid`` agrees with ``versor_condition (None or < ceiling)``; + * ``closure_observed`` agrees with "some beat observed a field"; + * ``closure_held`` agrees with "every OBSERVED versor_condition < ceiling"; + * ``heartbeats`` and the learning totals agree with the records; + * ``converged`` agrees with "records exist and the final beat did no work". + + This is the wrong=0 analogue for the continuity surface: a tampered artifact (a beat + whose ``field_valid`` lies about a breached ceiling, an inflated ``closure_held``, a + miscounted total) makes ``validate`` raise rather than render a false claim. + """ + + if record.status != "recorded": + return + + for beat in record.records: + expected_valid = ( + beat.versor_condition is None + or beat.versor_condition < record.closure_ceiling + ) + if beat.field_valid != expected_valid: + raise ValueError( + f"lived-life beat {beat.tick}: field_valid={beat.field_valid} disagrees " + f"with versor_condition vs ceiling " + f"({beat.versor_condition} / {record.closure_ceiling:.3e})" + ) + + observed = [ + b.versor_condition for b in record.records if b.versor_condition is not None + ] + if record.closure_observed != bool(observed): + raise ValueError( + "lived-life closure_observed disagrees with the per-beat measurements" + ) + if record.closure_held != all(vc < record.closure_ceiling for vc in observed): + raise ValueError( + "lived-life closure_held disagrees with the per-beat measurements" + ) + + if record.heartbeats != len(record.records): + raise ValueError("lived-life heartbeats disagrees with the record count") + if record.total_facts_consolidated != sum( + b.facts_consolidated for b in record.records + ): + raise ValueError( + "lived-life total_facts_consolidated disagrees with the records" + ) + if record.total_proposals_created != sum( + b.proposals_created for b in record.records + ): + raise ValueError( + "lived-life total_proposals_created disagrees with the records" + ) + + expected_converged = bool(record.records) and not record.records[-1].did_work + if record.converged != expected_converged: + raise ValueError("lived-life converged disagrees with the final beat") + + expected_resume = _resume_status(record.identity, record.current_identity) + if record.resume_status != expected_resume: + raise ValueError( + "lived-life resume_status disagrees with identity vs current_identity" + ) + if record.resume_summary != _RESUME_SUMMARY[record.resume_status]: + raise ValueError("lived-life resume_summary disagrees with resume_status") + + +def _coerce_heartbeat(raw: Any) -> LivedLifeHeartbeat: + if not isinstance(raw, dict): + raise ValueError("lived-life record must be an object") + versor_condition = raw.get("versor_condition") + return LivedLifeHeartbeat( + tick=int(raw["tick"]), + versor_condition=( + None if versor_condition is None else float(versor_condition) + ), + field_valid=bool(raw["field_valid"]), + facts_consolidated=int(raw["facts_consolidated"]), + proposals_created=int(raw["proposals_created"]), + pending_proposals=int(raw["pending_proposals"]), + did_work=bool(raw["did_work"]), + ) + + +def lived_life_from_payload( + raw: Any, + *, + artifact: ArtifactRef | None = None, + current_identity: str | None = None, +) -> LivedLife: + """Project a persisted ``lived_life.json`` payload into the validated read model. + + An unrecognized ``schema_version`` is honest absence (a forward/old artifact), not an + error. ``converged`` and ``resume_status`` are DERIVED here (not trusted from the + artifact) — ``resume_status`` from the persisted ``identity`` vs the caller-supplied + ``current_identity`` (the live substrate identity); ``validate`` then re-checks every + artifact-sourced claim against the records. + """ + + if not isinstance(raw, dict): + raise ValueError("lived_life artifact must be an object") + if raw.get("schema_version") != LIVED_LIFE_SCHEMA_VERSION: + return missing_lived_life( + f"unrecognized lived_life schema_version: {raw.get('schema_version')!r}" + ) + + records = [_coerce_heartbeat(item) for item in raw.get("records", [])] + identity = str(raw["identity"]) if raw.get("identity") else None + resume_status = _resume_status(identity, current_identity) + record = LivedLife( + schema_version="lived_life_v1", + status="recorded", + missing_reason=None, + identity=identity, + heartbeats=int(raw.get("heartbeats", len(records))), + closure_observed=bool(raw["closure_observed"]), + closure_held=bool(raw["closure_held"]), + closure_ceiling=float(raw.get("closure_ceiling", CLOSURE_CEILING)), + final_checkpoint_ok=bool(raw["final_checkpoint_ok"]), + converged=bool(records) and not records[-1].did_work, + total_facts_consolidated=int(raw["total_facts_consolidated"]), + total_proposals_created=int(raw["total_proposals_created"]), + current_identity=current_identity, + resume_status=resume_status, + resume_summary=_RESUME_SUMMARY[resume_status], + records=records, + artifact=artifact, + ) + validate(record) + return record diff --git a/workbench/readers.py b/workbench/readers.py index e91c1c6a..69459da1 100644 --- a/workbench/readers.py +++ b/workbench/readers.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from pathlib import Path, PurePosixPath from typing import Any, Callable, cast, get_args +from chat.always_on import LIVED_LIFE_FILENAME from core.config import RuntimeConfig from core.engine_identity import EngineIdentityError, engine_identity_for_config from engine_state import EngineStateStore, get_git_revision @@ -35,6 +36,7 @@ from workbench.schemas import ( IdentityContinuity, IdentityContinuityStatus, IdentityLineageRelation, + LivedLife, MathProposalDetail, MathProposalSummary, MathRatifyResult, @@ -50,6 +52,7 @@ from workbench.schemas import ( VaultEntry, VaultSummary, ) +from workbench.lived_life import lived_life_from_payload, missing_lived_life REPO_ROOT = Path(__file__).resolve().parents[1] SAFE_EVAL_LANES = frozenset({"contemplation_quality"}) @@ -326,6 +329,8 @@ def _artifact_kind(path: Path) -> str: rel = _relative(path) if rel == "engine_state/manifest.json": return "engine_state_manifest" + if rel == f"engine_state/{LIVED_LIFE_FILENAME}": + return "lived_life" if rel.startswith("teaching/proposals/"): return "proposal" if rel.startswith("evals/") and "/results/" in rel: @@ -1735,6 +1740,32 @@ def _identity_continuity_from_manifest( ) +def lived_life() -> LivedLife: + """Read the persisted always-on run (``engine_state/lived_life.json``) as the L10 + lived-life surface. + + Honest absence (``missing_evidence``) when no always-on run has been persisted yet — + the heartbeat (``chat.always_on.run_continuous`` + ``write_lived_life``) is what + produces the artifact; until a continuous-life run lands one, the surface says so + rather than fabricating a life.""" + path = ENGINE_STATE_ROOT / LIVED_LIFE_FILENAME + if not path.exists(): + return missing_lived_life("no always-on run has been persisted yet") + payload = _read_json_object(path) + artifact = _artifact_ref_for_path(path, "lived_life") + # The resume verdict: would a reboot resume THIS life? Recompute the current substrate + # identity with the canonical function (fail-soft -> "unknown", like IdentityContinuity). + try: + current_identity: str | None = engine_identity_for_config( + RuntimeConfig(), get_git_revision() + ) + except EngineIdentityError: + current_identity = None + return lived_life_from_payload( + payload, artifact=artifact, current_identity=current_identity + ) + + def _journal_entries(journal: Any) -> list[Any]: path = getattr(journal, "path", None) if isinstance(path, Path) and path.exists(): diff --git a/workbench/schemas.py b/workbench/schemas.py index d00f7e81..8eefe8c6 100644 --- a/workbench/schemas.py +++ b/workbench/schemas.py @@ -173,6 +173,67 @@ class FieldEvidence: transition_inner_product: float | None +@dataclass(frozen=True, slots=True) +class LivedLifeHeartbeat: + """One beat of the continuous life (read-only telemetry). + + Mirrors ``chat.always_on.HeartbeatRecord`` across the firewall: the closure of the + live field that beat (``versor_condition``, READ never repaired), whether it held the + ``< ceiling`` invariant, and what the life learned that beat (Step-D facts + + proposal-only proposals).""" + + tick: int + versor_condition: float | None + field_valid: bool + facts_consolidated: int + proposals_created: int + pending_proposals: int + did_work: bool + + +@dataclass(frozen=True, slots=True) +class LivedLife: + """L10 lived-life surface — evidence that CORE is ONE continuous life. + + A read-only projection of the persisted always-on run + (``chat.always_on.write_lived_life`` -> ``engine_state/lived_life.json``): the engine + holds itself alive over uptime with no user turn, learns while idle (Step-D + consolidation + proposal-only proposals), and holds closure BY CONSTRUCTION (the + heartbeat reads ``versor_condition`` as evidence, never repairs it). + + ``closure_held`` is consistency-checked at construction against the per-beat + measurements (``workbench.lived_life.validate``) — the surface can NEVER claim the + field stayed valid while a beat breached the ceiling (the wrong=0 analogue for the + continuity surface). ``converged`` is honest telemetry: a saturated life stops + churning (the final beat did no work). + + The two halves of "one continuous life" are both here: ``records`` are the lived + experience over uptime (T-experience), and ``resume_status`` is the resume guarantee + (T-resume) — it compares the life's content ``identity`` to the ``current_identity`` + recomputed from the live substrate. That is exactly the check the runtime load guard + makes on reboot (``would_resume`` ⟺ a reboot resumes THIS life; ``substrate_changed`` + ⟺ it would raise ``IdentityContinuityError``). The per-run lineage chain stays owned by + Runs ``IdentityContinuity``; this is the self-contained substrate verdict.""" + + schema_version: Literal["lived_life_v1"] + status: PipelineEvidenceStatus + missing_reason: str | None + identity: str | None + heartbeats: int + closure_observed: bool + closure_held: bool + closure_ceiling: float + final_checkpoint_ok: bool + converged: bool + total_facts_consolidated: int + total_proposals_created: int + current_identity: str | None + resume_status: Literal["would_resume", "substrate_changed", "unknown"] + resume_summary: str + records: list[LivedLifeHeartbeat] = field(default_factory=list) + artifact: ArtifactRef | None = None + + @dataclass(frozen=True, slots=True) class EvidenceBundle: """D3 shareable evidence bundle — a turn's deterministic evidence, citable.