Removes code_revision from the engine-identity hash: engine_identity is now the sha256 of the 5 ratified packs ONLY. The build revision is provenance (the manifest's written_at_revision), not identity — so a behavior-neutral rebuild is the SAME identity and the always-on daemon no longer flag-day strict-breaks on every commit (the ADR-0220 defect). Core: - core/engine_identity.py: ratified_substrate/compute_engine_identity drop the git_revision arg (packs-only); add compute_legacy_engine_identity (reproduces the pre-split packs+rev hash, for migration verification), ENGINE_IDENTITY_SCHEME=2, IdentityReconciliation enum, and reconcile_loaded_identity — the single source of truth for the runtime guard AND the workbench reader. - chat/runtime.py: the load guard reconciles via scheme. Current scheme -> direct packs-only compare. Legacy (code_revision-folded) stamp -> a VERIFYING migration: reconstruct the legacy hash from the persisted written_at_revision; a match proves packs unchanged (warn + re-stamp, resume, no break) — a mismatch means the packs genuinely changed (DIVERGED -> strict-refuse). Preserves 'distinct packs => refuse'. - engine_state/save_manifest: stamp identity_scheme alongside engine_identity (additive-optional; no schema_version bump). - workbench/readers.py: continuity reader uses the same reconcile (no phantom break on legacy checkpoints). cli.py break message reworded (packs, not 'build revision'). Callsite cleanup: drop the now-unused git_revision arg + imports across always_on.py, evals/l10_always_on/runner.py, workbench/readers.py. Tests: - test_identity_provenance_split.py (new): 5 reconcile unit proofs + 3 runtime integration proofs incl. the wrong=identity defense (legacy stamp of DIFFERENT packs still strict-refuses) and the re-stamp migration. - test_engine_identity.py: invert the code-revision test (rev no longer changes identity); assert the substrate is packs-only. - Restore 3 lineage tests silently red since ADR-0219 (flat-path _manifest helper now resolves the gen-dir). - Update remaining callsites/monkeypatches for the new signature. Hygiene: .gitignore now covers the ADR-0219 gen-dir runtime files (current, gen-*/, session_state.json, proposals.jsonl). Verified: 59 identity/migration/lineage/workbench + 32 L10 + 76 invariants/cli + 34 smoke pass; serving lane SHAs unchanged (no derivation/reliability_gate touch).
80 lines
3.2 KiB
Python
80 lines
3.2 KiB
Python
"""EngineIdentity lineage — L11 Phase 2.
|
|
|
|
Every checkpoint manifest stamps the content-derived ``engine_identity`` the
|
|
engine was running under, plus the ``parent_engine_identity`` of the prior
|
|
checkpoint — a git-like lineage chain. With a stable ratified substrate the
|
|
identity is constant (identity == parent: one stable life); a ratified change
|
|
would make identity != parent (the bump). Across reboot the chain is continuous:
|
|
the resumed life's checkpoint descends from the pre-reboot identity.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from chat.runtime import ChatRuntime
|
|
from core.cognition.pipeline import CognitiveTurnPipeline
|
|
from core.config import RuntimeConfig
|
|
from core.engine_identity import engine_identity_for_config
|
|
from engine_state import EngineStateStore
|
|
|
|
|
|
def _manifest(state_dir: Path) -> dict:
|
|
# Resolve via the store so the ADR-0219 generation-dir manifest is found
|
|
# (the committed manifest lives in gen-NNNN/, not at the flat root).
|
|
return EngineStateStore(state_dir).load_manifest() or {}
|
|
|
|
|
|
def _drive(state_dir: Path, n: int, config: RuntimeConfig | None = None) -> ChatRuntime:
|
|
runtime = ChatRuntime(config=config or RuntimeConfig(), engine_state_path=state_dir)
|
|
pipe = CognitiveTurnPipeline(runtime=runtime)
|
|
for _ in range(n):
|
|
pipe.run("What causes light?")
|
|
return runtime
|
|
|
|
|
|
def test_manifest_stamps_content_derived_engine_identity(tmp_path: Path) -> None:
|
|
state_dir = tmp_path / "es"
|
|
rt = _drive(state_dir, 1)
|
|
m = _manifest(state_dir)
|
|
assert m["engine_identity"] == rt._engine_identity
|
|
# It IS the content-derived identity of the ratified substrate (packs only).
|
|
assert m["engine_identity"] == engine_identity_for_config(RuntimeConfig())
|
|
|
|
|
|
def test_lineage_is_stable_across_a_continuous_run(tmp_path: Path) -> None:
|
|
state_dir = tmp_path / "es"
|
|
_drive(state_dir, 2) # 2 checkpoints
|
|
m = _manifest(state_dir)
|
|
# Stable substrate -> identity == parent (the life is one continuous identity).
|
|
assert m["engine_identity"] == m["parent_engine_identity"]
|
|
|
|
|
|
def test_lineage_is_continuous_across_reboot(tmp_path: Path) -> None:
|
|
state_dir = tmp_path / "es"
|
|
rt_a = _drive(state_dir, 2)
|
|
id_a = rt_a._engine_identity
|
|
|
|
# Reboot: a fresh runtime over the same checkpoint dir.
|
|
rt_b = ChatRuntime(config=RuntimeConfig(), engine_state_path=state_dir)
|
|
# The resumed life inherits the pre-reboot identity as its lineage parent.
|
|
assert rt_b._loaded_engine_identity == id_a
|
|
assert rt_b._engine_identity == id_a # same substrate -> same identity
|
|
|
|
pipe = CognitiveTurnPipeline(runtime=rt_b)
|
|
pipe.run("What is a concept?")
|
|
m = _manifest(state_dir)
|
|
assert m["parent_engine_identity"] == id_a # descends from the prior life
|
|
assert m["engine_identity"] == id_a
|
|
|
|
|
|
def test_different_identity_pack_is_a_different_identity(tmp_path: Path) -> None:
|
|
rt_default = ChatRuntime(
|
|
config=RuntimeConfig(), engine_state_path=tmp_path / "a", no_load_state=True
|
|
)
|
|
rt_precision = ChatRuntime(
|
|
config=RuntimeConfig(identity_pack="precision_first_v1"),
|
|
engine_state_path=tmp_path / "b",
|
|
no_load_state=True,
|
|
)
|
|
assert rt_default._engine_identity != rt_precision._engine_identity
|