dream_observation: drop the 'go quiet' rule from select_mode

The earlier behavior never went quiet — it dreamed every night, even when
that meant repeating itself. The 'return None on null delta' rule was a
synthesis-doc invention (the dreamer-design-spec.md I treated as
authoritative is itself LLM-generated) that didn't match the actual
desired UX. Aaron called this out.

The repetition problem the quiet rule was claimed to solve is already
addressed in the retrieve layer:
- LLM-generated queries from the observation signal vary nightly
- MMR diversity prevents within-night cluster lock-in
- NREM bias toward under-processed chunks (low consolidation_count)
  ensures fresh material gets selected over recently-replayed material

So select_mode now always returns a mode. NREM is the default. Staleness
still routes to Late REM at 3+ days for cross-domain variety. Journal
entries still route to Early REM.
This commit is contained in:
2026-05-22 23:49:27 +00:00
parent 3ec9a48151
commit 5582549321
+16 -28
View File
@@ -186,50 +186,38 @@ def observe_corpus():
# ─── Stage 2: select_mode ─────────────────────────────────────────────────── # ─── Stage 2: select_mode ───────────────────────────────────────────────────
def select_mode(signal, task=None, explicit_mode=None): def select_mode(signal, task=None, explicit_mode=None):
"""Return one of {'nrem', 'early-rem', 'late-rem', 'lucid'} or None. """Return one of {'nrem', 'early-rem', 'late-rem', 'lucid'}. Never None.
Selection logic from spec (lines 6974): The dreamer fires every scheduled night. The earlier "go quiet on null
- Explicit mode argument → use that mode delta" rule was a synthesis-doc invention that didn't match the actual
desired UX — the original dreamer always dreamed, even if it repeated
itself. The cure for repetition lives in the retrieve layer
(LLM-generated queries from the observation signal, MMR diversity,
cursor bias toward under-processed chunks), not in skipping nights.
Routing logic:
- explicit_mode argument wins
- task supplied → 'lucid' (question-anchored) - task supplied → 'lucid' (question-anchored)
- Active journal entry → Early REM (emotional/personal register) - days_since_dream ≥ STALENESS_TRIGGER_DAYS → 'late-rem' (shake loose
- Corpus unchanged ≥ STALENESS_TRIGGER_DAYS → Late REM (shake loose) via cross-domain pairs when nothing's been added in a while)
- New chunks above threshold → NREM - new journal entry → 'early-rem' (emotional/personal register)
- Otherwise → None ("dreamer goes quiet rather than manufacturing novelty") - default → 'nrem' (replay-and-consolidation; always has something to
do because the corpus always has under-processed chunks)
The None return is load-bearing. Per spec line 67, it's the canonical
answer to the repetition problem: when nothing has changed, do not
manufacture a dream just to fill the schedule slot.
""" """
if explicit_mode: if explicit_mode:
return explicit_mode return explicit_mode
if task: if task:
return "lucid" return "lucid"
new_chunks = signal["new_chunks"]
new_journal = signal["new_journal_entries"]
days_since = signal["days_since_dream"] days_since = signal["days_since_dream"]
new_journal = signal["new_journal_entries"]
# Spec line 72: corpus unchanged ≥3 days → Late REM ("shake things loose").
# This rule has to win against the "go quiet" rule below — the spec uses
# quiet as the default for 1-2 days of silence, then deliberately fires
# Late REM at 3+ days to break the stasis. Checking staleness first is
# what implements that intent. (Previous version checked "go quiet" first
# and never reached this branch.)
if days_since >= STALENESS_TRIGGER_DAYS: if days_since >= STALENESS_TRIGGER_DAYS:
return "late-rem" return "late-rem"
# Spec line 71: journal entry → Early REM
# We treat "any new journal entry" as the trigger. Refining to detect
# emotional/personal register requires LLM sentiment tagging, which
# is left for a later iteration.
if new_journal: if new_journal:
return "early-rem" return "early-rem"
# Spec line 67: nothing changed within the staleness window → quiet
if new_chunks < NEW_CHUNK_THRESHOLD and not new_journal:
return None
# Default: new chunks above threshold → NREM
return "nrem" return "nrem"