mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 22:34:06 +00:00
* refactor(dream): replace two-phase Dream class with simple cron + process_direct - Remove the heavyweight Dream class (AgentRunner-based two-phase system) from nanobot/agent/memory.py - Delete dream_phase1.md and dream_phase2.md templates - New dream.md template serves as the consolidation prompt - Cron callback uses agent.process_direct(prompt, session_key=\"dream\") instead of agent.dream.run() - Always performs git auto_commit after execution - /dream command updated to use process_direct + git commit - DreamConfig kept for backward compatibility; deprecated fields (model_override, max_batch_size, max_iterations, annotate_line_ages) are ignored but accepted in config - interval_h remains configurable via agents.defaults.dream.interval_h - Update tests and webui settings to match new architecture * feat(loop): add ephemeral mode to process_direct, skip history writes for Dream When ephemeral=True, _state_save skips enforce_file_cap (which calls raw_archive -> append_history) and consolidator.maybe_consolidate_by_tokens. This prevents Dream sessions from creating a positive feedback loop where they process their own output. The session IS still saved to disk. * fix(loop): skip extra hooks for ephemeral sessions (Dream) * feat(dream): per-run timestamped sessions with rotation for WebUI * test(config): restore DreamConfig schedule and alias tests * fix(dream): include LLM response summary in git auto-commit message The old two-phase Dream class included the Phase 1 analysis in the git commit message body. The new single-phase version lost this. Restore it by extracting resp.content from the process_direct return value and appending it to the commit message in both the cron handler and the /dream command. * fix(test): accept ephemeral kwarg in test_openai_api fake_process * refactor(dream): merge dream_session.py into MemoryStore The standalone dream_session.py module only contained three small helpers that all revolve around MemoryStore concerns (session keys, commit messages, file pruning). Fold them into MemoryStore as @staticmethod to reduce indirection and avoid a 35-line module with no independent reason to exist. * fix(test): address code review — patch correct instance, use actual function - Fix test_ephemeral_skips_raw_archive to patch loop.context.memory instead of the fixture's separate MemoryStore instance - Fix TestDreamCommitMessage to call MemoryStore.build_dream_commit_message instead of reimplementing the logic inline - Move Dream helpers in memory.py above the Consolidator section comment to avoid misleading visual boundary * fix(dream): gate cursor advancement and restrict tools maintainer edit: Dream now processes backlog from the oldest unprocessed entries, only advances the cursor after a completed ephemeral run, and uses a restricted file-only tool registry for background consolidation. * fix(dream): skip idle compact for dream sessions Dream runs use internal dream:* sessions that are pruned by Dream retention. Exclude them from AutoCompact scheduling, archive execution, and summary injection so idle-session compaction cannot truncate Dream transcripts. * fix(dream): keep batched history isolated * feat(dream): tag archived memory for single-phase Dream --------- Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
"""Tests for Dream session key generation and rotation."""
|
|
import time
|
|
from datetime import datetime
|
|
|
|
from nanobot.agent.memory import MemoryStore
|
|
|
|
|
|
class TestDreamSessionKey:
|
|
def test_contains_timestamp(self):
|
|
key = MemoryStore.dream_session_key()
|
|
assert key.startswith("dream:")
|
|
ts_part = key.split(":", 1)[1]
|
|
datetime.strptime(ts_part, "%Y%m%d-%H%M%S")
|
|
|
|
def test_unique_across_calls(self):
|
|
k1 = MemoryStore.dream_session_key()
|
|
time.sleep(1.1)
|
|
k2 = MemoryStore.dream_session_key()
|
|
assert k1 != k2
|
|
|
|
|
|
class TestPruneDreamSessions:
|
|
def test_keeps_n_most_recent(self, tmp_path):
|
|
sessions_dir = tmp_path / "sessions"
|
|
sessions_dir.mkdir()
|
|
|
|
for i in range(15):
|
|
key = f"dream:20260528-{100000 + i:06d}"
|
|
safe_key = key.replace(":", "_")
|
|
path = sessions_dir / f"{safe_key}.jsonl"
|
|
path.write_text(
|
|
f'{{"_type": "metadata", "key": "{key}", '
|
|
f'"created_at": "2026-05-28T10:00:{i:02d}", '
|
|
f'"updated_at": "2026-05-28T10:00:{i:02d}"}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
normal_path = sessions_dir / "telegram_123.jsonl"
|
|
normal_path.write_text('{"_type": "metadata"}\n', encoding="utf-8")
|
|
|
|
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
|
|
|
|
dream_files = sorted(sessions_dir.glob("dream_*.jsonl"))
|
|
assert len(dream_files) == 10
|
|
remaining_keys = [f.stem for f in dream_files]
|
|
assert "dream_20260528-100000" not in remaining_keys
|
|
assert "dream_20260528-100014" in remaining_keys
|
|
assert normal_path.exists()
|
|
|
|
def test_noop_when_under_limit(self, tmp_path):
|
|
sessions_dir = tmp_path / "sessions"
|
|
sessions_dir.mkdir()
|
|
for i in range(3):
|
|
key = f"dream:20260528-{100000 + i:06d}"
|
|
safe_key = key.replace(":", "_")
|
|
(sessions_dir / f"{safe_key}.jsonl").write_text("{}", encoding="utf-8")
|
|
|
|
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
|
|
assert len(list(sessions_dir.glob("dream_*.jsonl"))) == 3
|
|
|
|
def test_empty_dir_noop(self, tmp_path):
|
|
sessions_dir = tmp_path / "sessions"
|
|
sessions_dir.mkdir()
|
|
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
|