From dcf76117ab39dd238e125f514fdd44222ab04f96 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Wed, 17 Jun 2026 00:01:26 +0800 Subject: [PATCH] fix(command): explain empty dream runs --- nanobot/command/builtin.py | 20 ++++++++++- tests/command/test_builtin_dream.py | 56 +++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/nanobot/command/builtin.py b/nanobot/command/builtin.py index 6280e2df..d94fc4db 100644 --- a/nanobot/command/builtin.py +++ b/nanobot/command/builtin.py @@ -326,7 +326,8 @@ async def cmd_dream(ctx: CommandContext) -> OutboundMessage: if result is None: await loop.bus.publish_outbound(OutboundMessage( channel=msg.channel, chat_id=msg.chat_id, - content="Dream: nothing to process.", + content=_format_dream_no_input_message(), + metadata={"render_as": "text"}, )) return prompt, last_cursor = result @@ -374,6 +375,23 @@ async def cmd_dream(ctx: CommandContext) -> OutboundMessage: ) +def _format_dream_no_input_message() -> str: + return "\n".join([ + "Dream has no conversation history to process yet.", + "", + "Dream reads new entries from `memory/history.jsonl` after the current Dream cursor.", + ( + "Short chats only reach that file after token compaction or idle auto-compact, " + "so a fresh or short WebUI chat may leave Dream with no input." + ), + "", + "Next steps:", + "- Enable `agents.defaults.idleCompactAfterMinutes` so completed chats become Dream input automatically.", + "- Compact the current chat into memory once that manual action is available.", + "- If you expected history to exist, check whether `memory/history.jsonl` has new entries after the Dream cursor.", + ]) + + def _extract_changed_files(diff: str) -> list[str]: """Extract changed file paths from a unified diff.""" files: list[str] = [] diff --git a/tests/command/test_builtin_dream.py b/tests/command/test_builtin_dream.py index 7b1835fe..1a15055d 100644 --- a/tests/command/test_builtin_dream.py +++ b/tests/command/test_builtin_dream.py @@ -1,23 +1,32 @@ from __future__ import annotations +import asyncio from types import SimpleNamespace import pytest from nanobot.bus.events import InboundMessage -from nanobot.command.builtin import cmd_dream_log, cmd_dream_restore +from nanobot.command.builtin import cmd_dream, cmd_dream_log, cmd_dream_restore from nanobot.command.router import CommandContext from nanobot.utils.gitstore import CommitInfo class _FakeStore: - def __init__(self, git, last_dream_cursor: int = 1): + def __init__(self, git, last_dream_cursor: int = 1, dream_prompt_result=None): self.git = git self._last_dream_cursor = last_dream_cursor + self._dream_prompt_result = dream_prompt_result + self.compact_history_called = False def get_last_dream_cursor(self) -> int: return self._last_dream_cursor + def build_dream_prompt(self): + return self._dream_prompt_result + + def compact_history(self) -> None: + self.compact_history_called = True + class _FakeGit: def __init__( @@ -45,6 +54,17 @@ class _FakeGit: def revert(self, sha: str) -> str | None: return self._revert_result + def auto_commit(self, message: str) -> str | None: + return None + + +class _FakeBus: + def __init__(self): + self.outbound = [] + + async def publish_outbound(self, message): + self.outbound.append(message) + def _make_ctx(raw: str, git: _FakeGit, *, args: str = "", last_dream_cursor: int = 1) -> CommandContext: msg = InboundMessage(channel="cli", sender_id="u1", chat_id="direct", content=raw) @@ -53,6 +73,38 @@ def _make_ctx(raw: str, git: _FakeGit, *, args: str = "", last_dream_cursor: int return CommandContext(msg=msg, session=None, key=msg.session_key, raw=raw, args=args, loop=loop) +def _make_dream_ctx(tmp_path) -> tuple[CommandContext, _FakeBus]: + msg = InboundMessage(channel="cli", sender_id="u1", chat_id="direct", content="/dream") + store = _FakeStore(_FakeGit(initialized=False), dream_prompt_result=None) + bus = _FakeBus() + sessions_dir = tmp_path / "sessions" + sessions_dir.mkdir() + loop = SimpleNamespace( + bus=bus, + context=SimpleNamespace(memory=store, timezone="UTC"), + sessions=SimpleNamespace(sessions_dir=sessions_dir), + ) + ctx = CommandContext(msg=msg, session=None, key=msg.session_key, raw="/dream", args="", loop=loop) + return ctx, bus + + +@pytest.mark.asyncio +async def test_dream_no_history_explains_how_to_create_input(tmp_path) -> None: + ctx, bus = _make_dream_ctx(tmp_path) + + immediate = await cmd_dream(ctx) + await asyncio.sleep(0) + + assert immediate.content == "Dreaming..." + assert len(bus.outbound) == 1 + content = bus.outbound[0].content + assert "Dream has no conversation history to process yet." in content + assert "`memory/history.jsonl`" in content + assert "idle auto-compact" in content + assert "Dream cursor" in content + assert "agents.defaults.idleCompactAfterMinutes" in content + + @pytest.mark.asyncio async def test_dream_log_latest_is_more_user_friendly() -> None: commit = CommitInfo(sha="abcd1234", message="dream: 2026-04-04, 2 change(s)", timestamp="2026-04-04 12:00")