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).
This commit is contained in:
parent
ee970d8306
commit
f497667198
8 changed files with 276 additions and 6 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -61,6 +61,8 @@ engine_state/recognizers.jsonl
|
||||||
engine_state/discovery_candidates.jsonl
|
engine_state/discovery_candidates.jsonl
|
||||||
engine_state/lived_life.json
|
engine_state/lived_life.json
|
||||||
engine_state/always_on.lock
|
engine_state/always_on.lock
|
||||||
|
# per-life idle frontier-contemplation runs — any engine-state root (incl. CORE_ENGINE_STATE_DIR).
|
||||||
|
contemplation_runs/
|
||||||
|
|
||||||
# Private outreach / personal packet — never tracked
|
# Private outreach / personal packet — never tracked
|
||||||
01_Anthropic/
|
01_Anthropic/
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ class HeartbeatRecord:
|
||||||
proposals_created: int # reviewable proposals emitted this beat (proposal-only)
|
proposals_created: int # reviewable proposals emitted this beat (proposal-only)
|
||||||
pending_proposals: int
|
pending_proposals: int
|
||||||
did_work: bool
|
did_work: bool
|
||||||
|
frontier_findings: int = 0 # ADR-0080 SPECULATIVE findings mined this beat (0 unless enabled)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -227,6 +228,7 @@ def run_continuous(
|
||||||
result.facts_consolidated > 0
|
result.facts_consolidated > 0
|
||||||
or result.proposals_created > 0
|
or result.proposals_created > 0
|
||||||
or result.candidates_contemplated > 0
|
or result.candidates_contemplated > 0
|
||||||
|
or getattr(result, "frontier_findings", 0) > 0
|
||||||
)
|
)
|
||||||
record = HeartbeatRecord(
|
record = HeartbeatRecord(
|
||||||
tick=tick,
|
tick=tick,
|
||||||
|
|
@ -236,6 +238,7 @@ def run_continuous(
|
||||||
proposals_created=result.proposals_created,
|
proposals_created=result.proposals_created,
|
||||||
pending_proposals=result.pending_proposals,
|
pending_proposals=result.pending_proposals,
|
||||||
did_work=did_work,
|
did_work=did_work,
|
||||||
|
frontier_findings=getattr(result, "frontier_findings", 0),
|
||||||
)
|
)
|
||||||
records.append(record)
|
records.append(record)
|
||||||
if on_heartbeat is not None:
|
if on_heartbeat is not None:
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,10 @@ class IdleTickResult:
|
||||||
#: Step D — derived facts consolidated into the held self this tick (0 unless
|
#: Step D — derived facts consolidated into the held self this tick (0 unless
|
||||||
#: ``config.consolidate_determinations`` and the closure had a new layer to add).
|
#: ``config.consolidate_determinations`` and the closure had a new layer to add).
|
||||||
facts_consolidated: int = 0
|
facts_consolidated: int = 0
|
||||||
|
#: ADR-0080 — SPECULATIVE frontier findings autonomously mined + persisted this tick
|
||||||
|
#: (0 unless ``config.contemplate_frontier_during_idle`` AND a NOT-yet-mined frontier;
|
||||||
|
#: a mined frontier converges to 0). Reviewable via the HITL path; never ratified here.
|
||||||
|
frontier_findings: int = 0
|
||||||
#: IT — read-only proposal-review summary (None unless ``config.review_pending_proposals``).
|
#: IT — read-only proposal-review summary (None unless ``config.review_pending_proposals``).
|
||||||
#: Surfaces pending comprehension-failure proposals for review; mutates nothing.
|
#: Surfaces pending comprehension-failure proposals for review; mutates nothing.
|
||||||
proposal_review: ProposalReviewIdleSummary | None = None
|
proposal_review: ProposalReviewIdleSummary | None = None
|
||||||
|
|
@ -718,6 +722,10 @@ class ChatRuntime:
|
||||||
None if no_load_state else EngineStateStore(engine_state_path)
|
None if no_load_state else EngineStateStore(engine_state_path)
|
||||||
)
|
)
|
||||||
self._recognizer_registry: RecognizerRegistry = RecognizerRegistry()
|
self._recognizer_registry: RecognizerRegistry = RecognizerRegistry()
|
||||||
|
# Memoized frontier mine (ADR-0080 idle pass): the frontier reports are static within
|
||||||
|
# a session, so a converged life mines once instead of re-reading every idle beat. A
|
||||||
|
# reboot re-mines (a fresh runtime), picking up any frontier change across downtime.
|
||||||
|
self._frontier_run_cache: Any = None
|
||||||
self._turn_count: int = 0
|
self._turn_count: int = 0
|
||||||
self._pending_candidates: list[DiscoveryCandidate] = []
|
self._pending_candidates: list[DiscoveryCandidate] = []
|
||||||
self._pending_recognizer_examples: list[
|
self._pending_recognizer_examples: list[
|
||||||
|
|
@ -925,6 +933,33 @@ class ChatRuntime:
|
||||||
facts_consolidated = consolidate_once(self._context).consolidated
|
facts_consolidated = consolidate_once(self._context).consolidated
|
||||||
did_work = True
|
did_work = True
|
||||||
|
|
||||||
|
# 2b. Frontier-contemplation pass (ADR-0080 into the idle loop) — autonomously MINE
|
||||||
|
# the frontier-compare reports into persisted SPECULATIVE reviewable findings, so
|
||||||
|
# the always-on life proposes its OWN frontier with no user turn. Idempotent per
|
||||||
|
# frontier (an already-mined frontier is NOT re-persisted → the life converges, no
|
||||||
|
# churn); SPECULATIVE-only (ADR-0080: never COHERENT/ratified → the HITL path is
|
||||||
|
# untouched). Persists per-life under the engine-state dir.
|
||||||
|
frontier_findings = 0
|
||||||
|
if (
|
||||||
|
self.config.contemplate_frontier_during_idle
|
||||||
|
and self._engine_state_store is not None
|
||||||
|
):
|
||||||
|
from core.contemplation.runner import run_contemplation, write_contemplation_run
|
||||||
|
|
||||||
|
# Mine once per session (the frontier is static within a run); a reboot re-mines.
|
||||||
|
if self._frontier_run_cache is None:
|
||||||
|
self._frontier_run_cache = run_contemplation()
|
||||||
|
run = self._frontier_run_cache
|
||||||
|
target = (
|
||||||
|
self._engine_state_store.path
|
||||||
|
/ "contemplation_runs"
|
||||||
|
/ f"idle_{run.substrate_hash}.json"
|
||||||
|
)
|
||||||
|
if run.findings and not target.exists():
|
||||||
|
write_contemplation_run(run, target)
|
||||||
|
frontier_findings = len(run.findings)
|
||||||
|
did_work = True
|
||||||
|
|
||||||
# 3. Proposal-review sub-pass (IT) — READ-ONLY. Surfaces pending comprehension-failure
|
# 3. Proposal-review sub-pass (IT) — READ-ONLY. Surfaces pending comprehension-failure
|
||||||
# proposals (the contemplation pass's N5 artifacts) for review. It NEVER mutates an
|
# proposals (the contemplation pass's N5 artifacts) for review. It NEVER mutates an
|
||||||
# artifact, NEVER sets ``did_work`` (no state change → no checkpoint), NEVER ratifies
|
# artifact, NEVER sets ``did_work`` (no state change → no checkpoint), NEVER ratifies
|
||||||
|
|
@ -950,6 +985,7 @@ class ChatRuntime:
|
||||||
proposals_created=created,
|
proposals_created=created,
|
||||||
pending_proposals=self._count_pending_proposals(),
|
pending_proposals=self._count_pending_proposals(),
|
||||||
facts_consolidated=facts_consolidated,
|
facts_consolidated=facts_consolidated,
|
||||||
|
frontier_findings=frontier_findings,
|
||||||
proposal_review=proposal_review,
|
proposal_review=proposal_review,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
25
core/cli.py
25
core/cli.py
|
|
@ -353,9 +353,11 @@ def cmd_always_on(args: argparse.Namespace) -> int:
|
||||||
return
|
return
|
||||||
vc = "—" if record.versor_condition is None else f"{record.versor_condition:.2e}"
|
vc = "—" if record.versor_condition is None else f"{record.versor_condition:.2e}"
|
||||||
if record.did_work:
|
if record.did_work:
|
||||||
|
findings = getattr(record, "frontier_findings", 0)
|
||||||
|
findings_part = f" +{findings} findings" if findings else ""
|
||||||
print(
|
print(
|
||||||
f"[beat {record.tick}] learned +{record.facts_consolidated} facts "
|
f"[beat {record.tick}] learned +{record.facts_consolidated} facts "
|
||||||
f"+{record.proposals_created} proposals · closure {vc}",
|
f"+{record.proposals_created} proposals{findings_part} · closure {vc}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
elif record.tick % _ALWAYS_ON_ALIVE_EVERY == 0:
|
elif record.tick % _ALWAYS_ON_ALIVE_EVERY == 0:
|
||||||
|
|
@ -369,9 +371,18 @@ def cmd_always_on(args: argparse.Namespace) -> int:
|
||||||
f"core always-on: continuous-life heartbeat · interval {interval}s · {bound}",
|
f"core always-on: continuous-life heartbeat · interval {interval}s · {bound}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
daemon_config = _runtime_config_from_args(args)
|
||||||
|
if getattr(args, "contemplate_frontier", False):
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
# A learning daemon: autonomously mine the frontier into reviewable findings each
|
||||||
|
# idle beat (ADR-0080, SPECULATIVE + HITL). Opt-in — the default daemon stays idle.
|
||||||
|
daemon_config = dataclasses.replace(
|
||||||
|
daemon_config, contemplate_frontier_during_idle=True
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
result = run_daemon(
|
result = run_daemon(
|
||||||
config=_runtime_config_from_args(args),
|
config=daemon_config,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
max_beats=max_beats,
|
max_beats=max_beats,
|
||||||
no_load_state=bool(getattr(args, "no_load_state", False)),
|
no_load_state=bool(getattr(args, "no_load_state", False)),
|
||||||
|
|
@ -4260,6 +4271,16 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
default=False,
|
default=False,
|
||||||
help="suppress per-beat logging; print only the final summary",
|
help="suppress per-beat logging; print only the final summary",
|
||||||
)
|
)
|
||||||
|
always_on.add_argument(
|
||||||
|
"--contemplate-frontier",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
dest="contemplate_frontier",
|
||||||
|
help=(
|
||||||
|
"learning daemon: autonomously mine the frontier into reviewable SPECULATIVE "
|
||||||
|
"findings each idle beat (ADR-0080; never ratified, HITL-reviewed). Default off."
|
||||||
|
),
|
||||||
|
)
|
||||||
always_on.set_defaults(func=cmd_always_on)
|
always_on.set_defaults(func=cmd_always_on)
|
||||||
|
|
||||||
test = subparsers.add_parser("test", help="run pytest with curated suite aliases or direct passthrough")
|
test = subparsers.add_parser("test", help="run pytest with curated suite aliases or direct passthrough")
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,14 @@ class RuntimeConfig:
|
||||||
# OFF by default — idle ticks don't pay for the scan unless a deployment wants the surface.
|
# OFF by default — idle ticks don't pay for the scan unless a deployment wants the surface.
|
||||||
review_pending_proposals: bool = False
|
review_pending_proposals: bool = False
|
||||||
|
|
||||||
|
# Autonomous frontier contemplation (ADR-0080 into the idle loop) — when on, idle_tick
|
||||||
|
# autonomously MINES its frontier-compare reports into persisted SPECULATIVE reviewable
|
||||||
|
# findings (core.contemplation), so the always-on life proposes its own frontier without
|
||||||
|
# a user turn. Idempotent per frontier (a mined frontier converges → no churn); the
|
||||||
|
# findings are SPECULATIVE-only (ADR-0080: never COHERENT, never self-ratify) and reviewed
|
||||||
|
# via the existing HITL path. OFF by default — a deliberate learning-daemon mode.
|
||||||
|
contemplate_frontier_during_idle: bool = False
|
||||||
|
|
||||||
# Step E (ESTIMATION) — when on, a converse query the engine would otherwise REFUSE
|
# Step E (ESTIMATION) — when on, a converse query the engine would otherwise REFUSE
|
||||||
# (told p(a,b), asked p(b,a)) may be answered with a DISCLOSED [approximate] estimate
|
# (told p(a,b), asked p(b,a)) may be answered with a DISCLOSED [approximate] estimate
|
||||||
# IF the predicate-class has earned the SERVE license on the ratified, committed
|
# IF the predicate-class has earned the SERVE license on the ratified, committed
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
|
|
@ -177,12 +178,16 @@ def contemplate_contradiction_reports(
|
||||||
|
|
||||||
|
|
||||||
def write_contemplation_run(run: ContemplationRun, path: str | Path) -> None:
|
def write_contemplation_run(run: ContemplationRun, path: str | Path) -> None:
|
||||||
|
# Atomic write (temp + os.replace): the always-on life is an indefinite-uptime process
|
||||||
|
# where a SIGKILL/crash mid-write is expected, and the idle-pass skip-guard would NEVER
|
||||||
|
# repair a torn canonical file. A crash leaves only the orphan .tmp; the canonical path
|
||||||
|
# is either absent (re-mined next boot) or complete — never a half-written artifact.
|
||||||
target = Path(path)
|
target = Path(path)
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
target.write_text(
|
payload = json.dumps(run.as_dict(), ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||||||
json.dumps(run.as_dict(), ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
tmp = target.with_name(f".{target.name}.{os.getpid()}.tmp")
|
||||||
encoding="utf-8",
|
tmp.write_text(payload, encoding="utf-8")
|
||||||
)
|
os.replace(tmp, target)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
|
||||||
100
docs/analysis/afferent-field-ingest-scope-2026-06-15.md
Normal file
100
docs/analysis/afferent-field-ingest-scope-2026-06-15.md
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Afferent → Field Ingest Seam — Scope & Decision Brief
|
||||||
|
|
||||||
|
**Status:** scope (decision-gated; field-touching, CLAUDE.md sanctioned-site implication) ·
|
||||||
|
**Context:** frontier survey move #2, after the always-on process (#747/#748/#752) + its
|
||||||
|
long-horizon soak (#757) closed the *idle* lived-spine. This is the proposed next edge:
|
||||||
|
give the always-on life a **stream of experience** by folding compiled afferent
|
||||||
|
observations into the live cognitive field.
|
||||||
|
|
||||||
|
## The gap it would close
|
||||||
|
|
||||||
|
The always-on heartbeat lives, holds closure, and resumes — but it is a continuously
|
||||||
|
**idling** life: `idle_tick` drains a finite backlog + saturates deductive closure, then
|
||||||
|
does no work forever (proven by the convergence gate H3 in `evals/l10_always_on`). There is
|
||||||
|
**no afferent input channel** into the idle loop. Separately, the sensorium is fully built
|
||||||
|
as a compiler/frame/merge substrate but is **never wired to the field** — ADR-0208's own
|
||||||
|
pipeline arrow (`ObservationFrame → … → field / recall / cognition`) stops at the merge.
|
||||||
|
This seam is the missing edge in `listen → comprehend → … → replay`.
|
||||||
|
|
||||||
|
## What is confirmed (geometric feasibility)
|
||||||
|
|
||||||
|
- **Units carry a field-shaped target.** `CompilationUnitLike` (sensorium/compiler/protocol.py:28)
|
||||||
|
exposes `versor: np.ndarray` — a real CL(4,1) multivector — alongside `versor_condition`.
|
||||||
|
A live `ObservationFrame`'s units therefore expose targets in the field's algebra.
|
||||||
|
- **The sanctioned pull pattern applies directly, closure-preserving BY CONSTRUCTION.** The
|
||||||
|
exemplar is `session/context.py::_session_anchor_pull`:
|
||||||
|
```
|
||||||
|
R = word_transition_rotor(field_state.F, target) # rotor toward target
|
||||||
|
R_step = rotor_power(R, α) # fractional, stays on Spin manifold
|
||||||
|
pulled = versor_apply(R_step, field_state.F) # apply — no post-hoc unitize
|
||||||
|
```
|
||||||
|
It explicitly *replaced* the rejected `_slerp_toward` (which broke closure and needed a
|
||||||
|
forbidden post-hoc `unitize_versor`). An afferent pull toward `unit.versor` is structurally
|
||||||
|
identical → preserves `versor_condition < 1e-6` by construction, adds no normalization site.
|
||||||
|
- **Home:** `session/context.py` (the already-sanctioned site that owns the field), a new
|
||||||
|
opt-in `observe(frame)` that pulls the field toward each unit's versor by a fractional
|
||||||
|
step. Default off → serving + existing behavior byte-unchanged.
|
||||||
|
|
||||||
|
## The load-bearing wrinkle (the decision)
|
||||||
|
|
||||||
|
**Closure-preservation is necessary but NOT sufficient.** The sanctioned anchor-pull moves
|
||||||
|
the field toward a **session concept-attractor** — a target in the *cognitive* semantic
|
||||||
|
frame, carrying meaning ("the field anchors toward the session's concept"). An afferent
|
||||||
|
unit's versor is in the *same algebra* (CL(4,1)) but:
|
||||||
|
|
||||||
|
> **Is the sensorium's CGA embedding semantically COMMENSURABLE with the cognitive field —
|
||||||
|
> or merely same-algebra, different meaning?**
|
||||||
|
|
||||||
|
- If **commensurable**: pulling the field toward `unit.versor` is genuine perception →
|
||||||
|
cognitive state (decoding doctrine: the observation is real, the field moves toward it).
|
||||||
|
The seam is clean and meaningful.
|
||||||
|
- If **not** (same algebra, unrelated embedding): the pull is geometrically valid but
|
||||||
|
**semantically empty** — the field drifts toward an arbitrary point. That is exactly the
|
||||||
|
*degenerate fit* the field-as-standing-hand doctrine forbids ("geometrically-realizable
|
||||||
|
structure only; never force a degenerate fit").
|
||||||
|
|
||||||
|
This question is unestablished in the code: the sensorium compiles units into CL(4,1) but
|
||||||
|
nothing today asserts that embedding shares the cognitive field's semantic frame (they were
|
||||||
|
built as **disjoint** tracks — CLAUDE.md: the afferent arc "is disjoint from the GSM8K
|
||||||
|
serving path"). Wiring afferent → field is the *first* connection of the two.
|
||||||
|
|
||||||
|
## Decisions for ratification (Shay reviews the design)
|
||||||
|
|
||||||
|
1. **Commensurability ruling (the crux).** Is the sensorium CGA embedding intended to be
|
||||||
|
commensurable with the cognitive field? Three honest paths:
|
||||||
|
- **(a) Commensurable by construction** — if the modality packs project into the *same*
|
||||||
|
concept manifold the cognition field uses, the pull is meaningful → build the seam.
|
||||||
|
(Needs: evidence/assertion that the projection shares the frame, e.g. a shared
|
||||||
|
concept-attractor basis, before any pull is wired.)
|
||||||
|
- **(b) Bridge required** — define an explicit, falsifiable map from a unit's versor to a
|
||||||
|
cognitive-field target (a grounded embedding), so the pull is toward a *meant* point.
|
||||||
|
Larger; an ADR + a commensurability gate.
|
||||||
|
- **(c) Not yet** — keep the tracks disjoint; the always-on life stays idle-converging for
|
||||||
|
now; the stream-of-experience comes from a different intake (the autonomous
|
||||||
|
determination-frontier — roadmap Step 5 — which feeds *symbolic* new questions, not
|
||||||
|
afferent versors). Lower geometric risk, advances the telos via comprehension.
|
||||||
|
|
||||||
|
2. **Sanctioned-site clause.** If (a)/(b): extend `session/context.py`'s CLAUDE.md clause to
|
||||||
|
cover afferent-observation pull (closure-preserving by construction AND semantic =
|
||||||
|
perception→field), with the same bright-line justification. A CLAUDE.md amendment.
|
||||||
|
|
||||||
|
3. **ADR.** Completing ADR-0208's arrow (`ObservationFrame → field`) is the first
|
||||||
|
afferent→cognitive-core connection; it likely warrants an ADR (extend 0208 or new),
|
||||||
|
given the tracks were deliberately disjoint.
|
||||||
|
|
||||||
|
4. **Falsifiable gate (regardless of path).** Any pull that "preserves closure" is only
|
||||||
|
proven if a test fails when it doesn't: a soak guard (sibling to `evals/l10_always_on`)
|
||||||
|
asserting (i) `versor_condition < 1e-6` every observed beat, (ii) the same frame sequence
|
||||||
|
→ byte-identical field digest (determinism), (iii) a *commensurability* assertion — the
|
||||||
|
field moves **toward** the observation (`dist(F, target)` decreases), not arbitrarily.
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
The geometry is clean; the **semantics are the gate**. Recommend **ruling on decision 1
|
||||||
|
first** — do not wire a field pull until the commensurability question is answered, or the
|
||||||
|
seam risks a closure-valid-but-meaningless fit. If commensurability isn't yet established
|
||||||
|
(likely), the higher-leverage telos move is path **(c)**: the autonomous
|
||||||
|
determination-frontier intake (feed the idle heartbeat *symbolic* new questions it can
|
||||||
|
`determine` + commit wrong=0 / propose HITL) — which needs no field-embedding ruling and
|
||||||
|
directly converts the idle life into a learning one. The afferent→field seam then follows
|
||||||
|
once the embedding frame is established.
|
||||||
95
tests/test_idle_frontier_contemplation.py
Normal file
95
tests/test_idle_frontier_contemplation.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""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
|
||||||
Loading…
Reference in a new issue