* feat(l10): autonomous idle frontier-contemplation — the always-on life learns on its own Frontier survey move #2, path (c): give the always-on idle life a stream of experience via SYMBOLIC intake (not the afferent→field seam, which is deferred pending a commensurability ruling — docs/analysis/afferent-field-ingest-scope-2026-06-15.md). The idle heartbeat converges (idle_tick drains a finite backlog + saturates is-a closure, then does nothing forever); this makes it autonomously MINE its frontier with no user turn. Reuse-heavy (the mechanism largely existed): a gated idle pass wires ADR-0080 contemplation into the always-on loop. - core/config.py: contemplate_frontier_during_idle (default off → no behavior change). - chat/runtime.py: idle_tick pass 2b — when on, runs core.contemplation.run_contemplation() (mines the checked-in frontier-compare reports) and persists a reviewable ContemplationRun to <engine_state>/contemplation_runs/idle_<substrate_hash>.json. IDEMPOTENT per frontier (an already-mined frontier is not re-persisted → the life converges, no churn) and MEMOIZED per session (the static frontier is mined once, not re-read every beat). SPECULATIVE-only (ADR-0080: ContemplationFinding.__post_init__ RAISES if not SPECULATIVE → wrong=0 by construction; never ratified here — the HITL path is untouched). IdleTickResult gains frontier_findings. - chat/always_on.py: run_continuous did_work + HeartbeatRecord count frontier work, so the heartbeat/soak convergence (H3) stays honest. - core/cli.py: `core always-on --contemplate-frontier` (explicit learning-daemon mode) + honest per-beat log ("+N findings"). Adversarial 3-lens review (wrong=0/convergence, determinism/trust-boundary, test-vacuity) found 7; fixed the real ones: the MEDIUM torn-write (write_contemplation_run is now atomic temp+os.replace — the indefinite-uptime daemon expects SIGKILL mid-write, and the skip-guard would never repair a torn canonical file), the gitignore breadth (contemplation_runs/ at any engine-state root), the 64-bit filename truncation (full substrate_hash), and the re-mine-every-beat cost (session memoization). Deferred + documented: the shared contemplation miner embeds absolute report paths in finding CONTENT — a portability wrinkle that does NOT affect wrong=0/convergence/soak determinism (those key on the content-only substrate_hash) and touches shared code + its test surface; a follow-up. Tests (non-vacuous): 7 — off-by-default (no behavior change), mines-when-enabled, findings-SPECULATIVE (wrong=0), converges-idempotent, heartbeat-mines-then-converges, mines-once-per-session (memoization), atomic-no-orphan. 55 idle/contemplation/always-on + 96 invariants/contemplation + 28 smoke green; `core always-on --contemplate-frontier` mines end-to-end. engine_state contemplation_runs are per-life runtime state (gitignored). * fix(l10): isolate idle frontier pass + persist frontier_findings (#758) Three review blockers on the autonomous idle frontier-contemplation pass: 1. Daemon isolation. idle_tick's pass 2b called run_contemplation / write_contemplation_run bare inside the always-on loop (run_continuous wraps idle_tick in nothing but an exit-checkpoint finally), so a malformed frontier report or a transient FS fault would propagate out and KILL the continuous life. Wrap the optional pass best-effort (the exit-checkpoint boundary idiom): degrade to 0 findings + RuntimeWarning, leave did_work untouched, let a later beat retry. 2. Durable did_work explanation. HeartbeatRecord carried frontier_findings but serialize_report omitted it and AlwaysOnReport had no run total, so lived_life.json could show did_work=true with facts=0/proposals=0 and no persisted cause. Persist frontier_findings per-record + total_frontier_findings. 3. Honest HITL claim. The docstring/CLI/config said findings are "reviewable via the HITL path", but the existing HITL queue + workbench reader scan <repo>/contemplation/runs while the pass writes <engine_state>/contemplation_runs/. Narrow the claim to "persisted reviewable artifact, not yet in the HITL queue" (wiring that discovery is a follow-up). Tests: 4 new non-vacuous regressions (mining-failure degrade, write-failure degrade, always-on loop survives frontier failure, lived_life persists frontier_findings) — all fail against the unpatched pass. 38 L10/always-on/ workbench tests green.
172 lines
7.3 KiB
Python
172 lines
7.3 KiB
Python
"""ADR-0080 idle frontier-contemplation — the always-on life autonomously mines its
|
|
frontier into reviewable SPECULATIVE findings, with NO user turn.
|
|
|
|
Gated (``contemplate_frontier_during_idle``, default off → no behavior change); idempotent
|
|
per frontier (an already-mined frontier converges → no churn); SPECULATIVE-only (ADR-0080:
|
|
never COHERENT, never ratified here — the HITL path is untouched).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from chat.always_on import run_continuous, serialize_report
|
|
from chat.runtime import ChatRuntime
|
|
from core.config import RuntimeConfig
|
|
|
|
|
|
def _runtime(tmp_path, *, frontier: bool) -> ChatRuntime:
|
|
config = dataclasses.replace(
|
|
RuntimeConfig(), contemplate_frontier_during_idle=frontier
|
|
)
|
|
return ChatRuntime(config=config, engine_state_path=tmp_path / "es")
|
|
|
|
|
|
def _run_files(tmp_path) -> list:
|
|
d = tmp_path / "es" / "contemplation_runs"
|
|
return sorted(d.glob("idle_*.json")) if d.exists() else []
|
|
|
|
|
|
def test_idle_frontier_off_by_default(tmp_path) -> None:
|
|
# Default config (flag off): no mining, no run persisted — behavior unchanged.
|
|
result = _runtime(tmp_path, frontier=False).idle_tick()
|
|
assert result.frontier_findings == 0
|
|
assert _run_files(tmp_path) == []
|
|
|
|
|
|
def test_idle_frontier_mines_when_enabled(tmp_path) -> None:
|
|
result = _runtime(tmp_path, frontier=True).idle_tick()
|
|
assert result.frontier_findings >= 1 # autonomously mined a finding, no user turn
|
|
assert len(_run_files(tmp_path)) == 1 # persisted one reviewable run
|
|
# Atomic write: a complete artifact, no orphan temp left at the canonical path.
|
|
runs_dir = tmp_path / "es" / "contemplation_runs"
|
|
assert not list(runs_dir.glob(".*.tmp"))
|
|
json.loads(_run_files(tmp_path)[0].read_text(encoding="utf-8")) # valid (not torn)
|
|
|
|
|
|
def test_idle_frontier_mines_once_per_session(tmp_path, monkeypatch) -> None:
|
|
# A converged life must not re-read/parse/hash the frontier every beat (indefinite
|
|
# uptime): the static frontier is mined ONCE per session and memoized.
|
|
import core.contemplation.runner as runner_mod
|
|
|
|
real = runner_mod.run_contemplation
|
|
calls = {"n": 0}
|
|
|
|
def _spy():
|
|
calls["n"] += 1
|
|
return real()
|
|
|
|
monkeypatch.setattr(runner_mod, "run_contemplation", _spy)
|
|
rt = _runtime(tmp_path, frontier=True)
|
|
rt.idle_tick()
|
|
rt.idle_tick()
|
|
rt.idle_tick()
|
|
assert calls["n"] == 1
|
|
|
|
|
|
def test_idle_frontier_findings_are_speculative(tmp_path) -> None:
|
|
# wrong=0 / ADR-0080: an autonomously-mined finding is SPECULATIVE, never COHERENT.
|
|
_runtime(tmp_path, frontier=True).idle_tick()
|
|
run = json.loads(_run_files(tmp_path)[0].read_text(encoding="utf-8"))
|
|
findings = run["findings"]
|
|
assert findings
|
|
assert all(f["epistemic_status"] == "speculative" for f in findings)
|
|
|
|
|
|
def test_idle_frontier_converges_idempotent(tmp_path) -> None:
|
|
# A mined frontier is not re-mined — the life converges (no churn), the wrong=0 invariant
|
|
# for a continuous life: an idle beat over an exhausted frontier does no work.
|
|
rt = _runtime(tmp_path, frontier=True)
|
|
first = rt.idle_tick()
|
|
second = rt.idle_tick()
|
|
assert first.frontier_findings >= 1
|
|
assert second.frontier_findings == 0 # already mined this frontier
|
|
assert len(_run_files(tmp_path)) == 1 # no duplicate run persisted
|
|
|
|
|
|
def test_heartbeat_mines_then_converges(tmp_path) -> None:
|
|
# The always-on heartbeat over a frontier-contemplating life: beat 0 mines (did_work via
|
|
# frontier_findings), then the saturated life settles to rest.
|
|
rt = _runtime(tmp_path, frontier=True)
|
|
report = run_continuous(rt, heartbeats=4)
|
|
assert report.records[0].did_work is True # beat 0 mined the frontier
|
|
assert report.records[-1].did_work is False # converged to rest
|
|
assert len(_run_files(tmp_path)) == 1
|
|
|
|
|
|
def test_idle_frontier_pass_degrades_on_mining_failure(tmp_path, monkeypatch) -> None:
|
|
# The frontier mine is an OPTIONAL pass on an indefinite-uptime life: a failure in
|
|
# run_contemplation (e.g. a malformed frontier report) must DEGRADE to 0 findings and
|
|
# warn, NEVER crash idle_tick. A converged continuous life cannot be killed by an
|
|
# optional background pass.
|
|
import core.contemplation.runner as runner_mod
|
|
|
|
def _boom():
|
|
raise ValueError("malformed frontier report")
|
|
|
|
monkeypatch.setattr(runner_mod, "run_contemplation", _boom)
|
|
rt = _runtime(tmp_path, frontier=True)
|
|
with pytest.warns(RuntimeWarning, match="idle frontier-contemplation pass skipped"):
|
|
result = rt.idle_tick() # must NOT raise
|
|
assert result.frontier_findings == 0 # degraded, no findings this beat
|
|
assert _run_files(tmp_path) == [] # nothing persisted on failure
|
|
|
|
|
|
def test_idle_frontier_pass_degrades_on_write_failure(tmp_path, monkeypatch) -> None:
|
|
# A transient filesystem fault mid-persist must also degrade, not crash. The run-cache
|
|
# is mined, but the write fails → 0 findings + warning, loop survives.
|
|
import core.contemplation.runner as runner_mod
|
|
|
|
def _boom(*_args, **_kwargs):
|
|
raise OSError("disk full")
|
|
|
|
monkeypatch.setattr(runner_mod, "write_contemplation_run", _boom)
|
|
rt = _runtime(tmp_path, frontier=True)
|
|
with pytest.warns(RuntimeWarning, match="idle frontier-contemplation pass skipped"):
|
|
result = rt.idle_tick() # must NOT raise
|
|
assert result.frontier_findings == 0
|
|
|
|
|
|
def test_always_on_loop_survives_frontier_failure(tmp_path, monkeypatch) -> None:
|
|
# End-to-end: the always-on heartbeat completes all beats and persists its final
|
|
# checkpoint even when every frontier mine fails — the continuous life is not killed.
|
|
import core.contemplation.runner as runner_mod
|
|
|
|
monkeypatch.setattr(
|
|
runner_mod,
|
|
"run_contemplation",
|
|
lambda: (_ for _ in ()).throw(ValueError("boom")),
|
|
)
|
|
rt = _runtime(tmp_path, frontier=True)
|
|
with pytest.warns(RuntimeWarning):
|
|
report = run_continuous(rt, heartbeats=3)
|
|
assert report.heartbeats == 3 # ran to completion, no crash
|
|
assert report.final_checkpoint_ok is True
|
|
assert report.total_frontier_findings == 0
|
|
assert all(r.frontier_findings == 0 for r in report.records)
|
|
|
|
|
|
def test_lived_life_persists_frontier_findings(tmp_path) -> None:
|
|
# did_work must have a durable explanation: when the only work of a beat is a frontier
|
|
# mine (facts/proposals both 0), the persisted lived-life record must still show the
|
|
# cause — frontier_findings per-record AND a run total — not an unexplained did_work.
|
|
rt = _runtime(tmp_path, frontier=True)
|
|
report_path = tmp_path / "es" / "lived_life.json"
|
|
report = run_continuous(rt, heartbeats=2, report_path=report_path)
|
|
|
|
assert report.total_frontier_findings >= 1
|
|
beat0 = report.records[0]
|
|
assert beat0.did_work is True
|
|
# The mine — not facts/proposals — is what made beat 0 work; the record proves it.
|
|
assert beat0.facts_consolidated == 0
|
|
assert beat0.proposals_created == 0
|
|
assert beat0.frontier_findings >= 1
|
|
|
|
persisted = json.loads(report_path.read_text(encoding="utf-8"))
|
|
assert persisted["total_frontier_findings"] >= 1
|
|
assert persisted["records"][0]["frontier_findings"] >= 1
|
|
# serialize_report (the workbench projection) carries the same durable explanation.
|
|
assert serialize_report(report)["records"][0]["frontier_findings"] >= 1
|