core/tests/test_idle_frontier_contemplation.py
Shay f497667198 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).
2026-06-14 18:56:07 -07:00

95 lines
3.8 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
from chat.always_on import run_continuous
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