Files
project-lyra/tests/test_dream.py
T
serversdown 29a4d59661 fix: cap MI50 summary length + fast-fail cloud fallback
The dream cycle's summarize_all ran uncapped against the MI50: no max_tokens
and no timeout, so the OpenAI SDK's 600s x2-retry default meant ~30 min per
call. Combined with summary.py's own retry loop, one unsummarizable session
pegged the GPU for hours (observed 2026-07-04: stuck since 23:02, nothing saved
since 00:56, 7-8k-token runaway generations, all 4 llama.cpp slots busy). Not
context overflow (0 shifts/truncations) - purely unbounded length on a slow
backend timing out and retrying.

- llm.complete(): add optional max_tokens (caps generation; num_predict for
  Ollama) and timeout (bounds the request and sets max_retries=0 so the caller
  owns retry policy). Both default None -> unchanged for every existing caller.
- summary.py: cap gists at 768 tokens, 150s/call fast-fail, 2 MI50 attempts
  then one cloud fallback (when primary isn't already cloud and a key exists).

Known limitation (scoped out per decision): the fallback triggers on
timeouts/exceptions, not on a degraded backend returning garbage as a 200.

Tests: fallback fires after 2 MI50 failures; no fallback when primary is cloud
or no key; cap+timeout threaded into every complete() call; llm bounds tests.
172 pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yrEb5qpPGv2FjyxrB7LLk
2026-07-04 07:19:31 +00:00

80 lines
3.0 KiB
Python

"""Dream-cycle tests: backlog sensing + a full forced pass, with LLM/embeddings
stubbed so nothing hits a real backend."""
from __future__ import annotations
import importlib
import pytest
@pytest.fixture
def lyra(tmp_path, monkeypatch):
"""A fresh Lyra wired to a temp DB with stubbed embeddings + LLM."""
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
monkeypatch.setenv("SUMMARY_BACKEND", "local")
monkeypatch.setenv("LYRA_FEEDS", "") # dream cycle refreshes feeds; keep it offline
from lyra import llm
# Deterministic 3-d embeddings; content-insensitive is fine for storage tests.
monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts])
# reflect() expects JSON back; everything else just stores the text.
monkeypatch.setattr(
llm, "complete",
lambda messages, backend=None, model=None, **_:
'{"mood":"focused","valence":0.7,"new_reflections":["I got some thinking done."]}',
)
import lyra.memory as memory
importlib.reload(memory) # drop any cached connection from another test/db
return memory
def _seed(memory, session_id, n, summarized_up_to=None):
ids = [memory.remember(session_id, "user", f"msg {i}") for i in range(n)]
if summarized_up_to is not None:
memory.store_summary(session_id, "gist", ids[summarized_up_to])
return ids
def test_backlog_stats(lyra):
memory = lyra
_seed(memory, "s-fresh", 5) # never summarized -> ripe
_seed(memory, "s-ripe", 25, summarized_up_to=0) # 24 new turns -> ripe
_seed(memory, "s-clean", 3, summarized_up_to=2) # caught up -> not dirty
stats = memory.backlog_stats(ripe_threshold=20)
assert stats["sessions"] == 3
assert stats["dirty"] == 2
assert stats["ripe"] == 2
assert stats["max_exchange_id"] == 33
def test_dream_cycle_consolidates_and_persists(lyra):
memory = lyra
from lyra import dream
# A big backlog: enough never-summarized sessions that continuity saturates
# and the resulting fresh gists push coherence past threshold too.
for k in range(7):
_seed(memory, f"s{k}", 4)
state = dream.dream_cycle(force=False)
# continuity built up and fired -> sessions got summarized
assert len(memory.list_summaries()) == 7
acts = state["dream"]["last_actions"]
assert any("consolidated" in a for a in acts)
# 7 fresh gists -> coherence crossed threshold -> profile got integrated
assert any("integrated" in a for a in acts)
assert memory.get_profile() is not None
# drives + bookkeeping persisted and reload-able
assert set(state["drives"]) == {"continuity", "coherence", "curiosity", "stability"}
assert state["dream"]["cycle_count"] == 1
assert memory.get_self_state()["dream"]["last_exchange_id"] == 28
# a second pass with no new activity should rest (continuity relieved)
state2 = dream.dream_cycle(force=False)
assert state2["dream"]["cycle_count"] == 2
assert state2["drives"]["continuity"] == 0.0