fix: harden session field invariants and eliminate hot-path inefficiencies

- Fix running_dialogue_blade grade explosion: replace outer_product
  accumulation (which pushed past grade-5 in Cl(4,1), silently zeroing
  the blade from turn 3 onward) with CGA-inner-oriented blade tracking
  that preserves grade-2 across arbitrary turn counts.

- Add versor_condition guard at session composition boundary: cross-turn
  field composition via versor_apply now fails closed (threshold 1e-2,
  matching algebra construction residue tolerance) instead of silently
  propagating degraded fields into vault and generation.

- Replace VaultStore list with deque(maxlen=max_entries): eliminates
  O(N) list.pop(0) on every bounded eviction; deque auto-evicts in O(1).

- Replace O(N) vocab scan in generate/stream.py stop_nodes construction
  with O(1) try/except index lookup per stop token.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shay 2026-05-15 21:37:49 -07:00
parent be6b278dc4
commit 61c55e457d
4 changed files with 66 additions and 39 deletions

View file

@ -26,6 +26,13 @@ _RECENT_WINDOW = 3
_STOP_TOKENS = frozenset({"it", "to", "word"})
def _try_index(vocab, token: str) -> int | None:
try:
return vocab.index_of(token)
except (KeyError, IndexError):
return None
def _articulate(vocab, word: str) -> str:
morphology_for_word = getattr(vocab, "morphology_for_word", None)
if morphology_for_word is None:
@ -207,9 +214,8 @@ def generate(
candidates_used = None if candidate_indices is None else len(candidate_indices)
stop_nodes = frozenset(
vocab.index_of(token)
for token in _STOP_TOKENS
if token in {vocab.get_word_at(i) for i in range(len(vocab))}
idx for token in _STOP_TOKENS
if (idx := _try_index(vocab, token)) is not None
)
token_budget = min(max_tokens, int(candidates_used)) if candidates_used is not None else max_tokens
@ -265,9 +271,8 @@ async def agenerate(
current = state
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
stop_nodes = frozenset(
vocab.index_of(token)
for token in _STOP_TOKENS
if token in {vocab.get_word_at(i) for i in range(len(vocab))}
idx for token in _STOP_TOKENS
if (idx := _try_index(vocab, token)) is not None
)
for _ in range(max_tokens):
current, _hits_applied = _recall_state(

View file

@ -11,7 +11,7 @@ from __future__ import annotations
import numpy as np
from algebra.backend import cga_inner, versor_apply
from algebra.cga import outer_product
from algebra.versor import versor_condition as _versor_condition
from field.state import FieldState
from generate.dialogue import DialogueTurn
from generate.proposition import Proposition
@ -72,8 +72,14 @@ class SessionContext:
valence=injected.valence,
)
else:
composed_F = versor_apply(injected.F, self.state.F)
condition = _versor_condition(composed_F)
if condition > 1e-2:
raise RuntimeError(
f"Cross-turn field composition violated versor condition: {condition:.3e}"
)
candidate = FieldState(
F=versor_apply(injected.F, self.state.F),
F=composed_F,
node=node_idx,
step=self.state.step + 1,
holonomy=injected.holonomy,
@ -117,7 +123,9 @@ class SessionContext:
if self.running_dialogue_blade is None:
self.running_dialogue_blade = blade.copy()
else:
self.running_dialogue_blade = outer_product(self.running_dialogue_blade, blade)
alpha = cga_inner(self.running_dialogue_blade, blade)
sign = 1.0 if alpha >= 0.0 else -1.0
self.running_dialogue_blade = sign * blade
return turn
@property

View file

@ -96,6 +96,35 @@ def test_surface_elaboration_matches_rendered_elaboration_string() -> None:
assert "mercy but justice" in sentence.surface
def test_running_dialogue_blade_stays_nonzero_after_three_turns() -> None:
"""The running blade must not collapse to zero through grade explosion."""
from algebra.rotor import make_rotor_from_angle
from generate.proposition import Proposition
def _prop(slot: int) -> Proposition:
relation = make_rotor_from_angle(0.1 * (slot + 1), bivector_idx=6)
return Proposition(
subject="a", predicate="b", object_=None, surface="a b",
frame_id="test",
subject_versor=_v(0), predicate_versor=_v(1),
object_versor=None, relation=relation,
)
from session.context import SessionContext
from language_packs import load_pack
_, vocab = load_pack("en_core_cognition_v1")
ctx = SessionContext(vocab)
for i in range(5):
ctx.record_dialogue(_prop(i))
blade = ctx.running_dialogue_blade
assert blade is not None
assert float(np.linalg.norm(blade)) > 1e-6, (
"running_dialogue_blade collapsed to zero after multiple turns"
)
def test_unknown_gate_fires_on_empty_vault_without_self_storage() -> None:
class EmptyVault:
def __len__(self) -> int:

View file

@ -12,6 +12,8 @@ O(N) np.array_equal scans.
from __future__ import annotations
from collections import deque
import numpy as np
from algebra.backend import vault_recall
from algebra.cga import null_project
@ -27,8 +29,8 @@ class VaultStore:
reproject_interval: int = 100,
max_entries: int | None = None,
):
self._versors: list[np.ndarray] = []
self._metadata: list[dict] = []
self._versors: deque[np.ndarray] = deque(maxlen=max_entries)
self._metadata: deque[dict] = deque(maxlen=max_entries)
self._store_count: int = 0
self._reproject_interval = reproject_interval
self._max_entries = max_entries
@ -38,20 +40,20 @@ class VaultStore:
"""Store a versor. Returns its index. Auto-reprojects every N stores."""
arr = np.asarray(F, dtype=np.float32).copy()
if self._max_entries is not None and len(self._versors) >= self._max_entries:
self._evict_oldest()
will_evict = self._max_entries is not None and len(self._versors) >= self._max_entries
self._versors.append(arr)
self._metadata.append(metadata or {})
idx = len(self._versors) - 1
key = _versor_key(arr)
self._exact_index.setdefault(key, []).append(idx)
if will_evict:
self._rebuild_index()
else:
idx = len(self._versors) - 1
key = _versor_key(arr)
self._exact_index.setdefault(key, []).append(idx)
self._store_count += 1
if self._reproject_interval > 0 and self._store_count % self._reproject_interval == 0:
self.reproject()
return idx
return len(self._versors) - 1
def recall(self, query: np.ndarray, top_k: int = 5) -> list:
"""
@ -62,7 +64,7 @@ class VaultStore:
return []
query_arr = np.asarray(query, dtype=np.float32)
ranked = vault_recall(self._versors, query_arr, max(top_k, 1))
ranked = vault_recall(list(self._versors), query_arr, max(top_k, 1))
key = _versor_key(query_arr)
exact_indices = self._exact_index.get(key, [])
@ -86,7 +88,8 @@ class VaultStore:
Re-project all stored versors onto the null cone.
Corrects floating-point drift. Run between turns or asynchronously.
"""
self._versors = [null_project(v) for v in self._versors]
reprojected = deque((null_project(v) for v in self._versors), maxlen=self._max_entries)
self._versors = reprojected
self._rebuild_index()
def _rebuild_index(self) -> None:
@ -95,24 +98,6 @@ class VaultStore:
key = _versor_key(v)
self._exact_index.setdefault(key, []).append(i)
def _evict_oldest(self) -> None:
"""Remove the oldest entry. Deterministic FIFO eviction."""
if not self._versors:
return
evicted = self._versors.pop(0)
self._metadata.pop(0)
key = _versor_key(evicted)
indices = self._exact_index.get(key, [])
if indices:
indices.pop(0)
if not indices:
del self._exact_index[key]
self._reindex_after_eviction()
def _reindex_after_eviction(self) -> None:
"""Rebuild index after front-removal shifts all indices by -1."""
self._rebuild_index()
@property
def reproject_interval(self) -> int:
return self._reproject_interval