mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(dream): gate periodic commits on content changes
This commit is contained in:
parent
9adcd3d923
commit
c111aaa7ee
@ -181,6 +181,20 @@ def _advance_dream_cursor_if_behind(memory: Any) -> None:
|
|||||||
memory.set_last_dream_cursor(latest)
|
memory.set_last_dream_cursor(latest)
|
||||||
|
|
||||||
|
|
||||||
|
def _commit_dream_changes(memory: Any) -> str | None:
|
||||||
|
"""Commit durable Dream edits, without entering the commit path for a no-op run."""
|
||||||
|
if not memory.git.is_initialized():
|
||||||
|
return None
|
||||||
|
diff_body = memory.dream_content_diff()
|
||||||
|
if not diff_body:
|
||||||
|
return None
|
||||||
|
message = memory.build_dream_commit_message(
|
||||||
|
"dream: periodic memory consolidation",
|
||||||
|
diff_body,
|
||||||
|
)
|
||||||
|
return memory.git.auto_commit(message)
|
||||||
|
|
||||||
|
|
||||||
class SafeFileHistory(FileHistory):
|
class SafeFileHistory(FileHistory):
|
||||||
"""FileHistory subclass that sanitizes surrogate characters on write.
|
"""FileHistory subclass that sanitizes surrogate characters on write.
|
||||||
|
|
||||||
@ -1508,7 +1522,6 @@ def _run_gateway(
|
|||||||
from nanobot.agent.memory import MemoryStore
|
from nanobot.agent.memory import MemoryStore
|
||||||
|
|
||||||
dream_session_key = MemoryStore.dream_session_key
|
dream_session_key = MemoryStore.dream_session_key
|
||||||
build_dream_commit_message = MemoryStore.build_dream_commit_message
|
|
||||||
prune_dream_sessions = MemoryStore.prune_dream_sessions
|
prune_dream_sessions = MemoryStore.prune_dream_sessions
|
||||||
|
|
||||||
store = agent.context.memory
|
store = agent.context.memory
|
||||||
@ -1537,13 +1550,6 @@ def _run_gateway(
|
|||||||
if productive:
|
if productive:
|
||||||
store.set_last_dream_cursor(last_cursor)
|
store.set_last_dream_cursor(last_cursor)
|
||||||
logger.info("Dream cron job completed, cursor advanced to {}", last_cursor)
|
logger.info("Dream cron job completed, cursor advanced to {}", last_cursor)
|
||||||
if store.git.is_initialized():
|
|
||||||
msg = build_dream_commit_message(
|
|
||||||
"dream: periodic memory consolidation", diff_body,
|
|
||||||
)
|
|
||||||
sha = store.git.auto_commit(msg)
|
|
||||||
if sha:
|
|
||||||
logger.info("Dream commit: {}", sha)
|
|
||||||
elif MemoryStore.dream_run_completed(resp):
|
elif MemoryStore.dream_run_completed(resp):
|
||||||
logger.info(
|
logger.info(
|
||||||
"Dream cron job completed with no memory changes; "
|
"Dream cron job completed with no memory changes; "
|
||||||
@ -1564,6 +1570,9 @@ def _run_gateway(
|
|||||||
source="dream",
|
source="dream",
|
||||||
timezone_name=config.agents.defaults.timezone,
|
timezone_name=config.agents.defaults.timezone,
|
||||||
)
|
)
|
||||||
|
sha = _commit_dream_changes(store)
|
||||||
|
if sha:
|
||||||
|
logger.info("Dream commit: {}", sha)
|
||||||
store.compact_history()
|
store.compact_history()
|
||||||
prune_dream_sessions(agent.sessions.sessions_dir)
|
prune_dream_sessions(agent.sessions.sessions_dir)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -654,53 +654,6 @@ class TestDreamCommitMessage:
|
|||||||
== "dream: x\n\nSOUL.md: +1 -0"
|
== "dream: x\n\nSOUL.md: +1 -0"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_productive_gate_skips_commit_when_no_content_change(self, tmp_path):
|
|
||||||
"""When no content files changed, there should be no auto_commit.
|
|
||||||
In the old code, auto_commit ran unconditionally
|
|
||||||
in the finally block; now it only runs when productive is True."""
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
store = MemoryStore(tmp_path)
|
|
||||||
store.write_soul("# Soul")
|
|
||||||
store.write_memory("# Memory")
|
|
||||||
store.git.init()
|
|
||||||
store.git.auto_commit("initial")
|
|
||||||
|
|
||||||
assert store.dream_content_diff() == ""
|
|
||||||
|
|
||||||
original = store.git.auto_commit
|
|
||||||
store.git.auto_commit = MagicMock(wraps=original)
|
|
||||||
|
|
||||||
# Exact productive gate pattern from the cron handler (commands.py):
|
|
||||||
diff_body = store.dream_content_diff()
|
|
||||||
productive = bool(diff_body) # git is initialized
|
|
||||||
if productive and store.git.is_initialized():
|
|
||||||
store.git.auto_commit("dream: periodic memory consolidation")
|
|
||||||
|
|
||||||
store.git.auto_commit.assert_not_called()
|
|
||||||
|
|
||||||
def test_productive_gate_allows_commit_on_content_change(self, tmp_path):
|
|
||||||
"""The productive gate must perform auto_commit when content files changed."""
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
store = MemoryStore(tmp_path)
|
|
||||||
store.write_soul("# Soul")
|
|
||||||
store.write_memory("# Memory")
|
|
||||||
store.git.init()
|
|
||||||
store.git.auto_commit("initial")
|
|
||||||
|
|
||||||
store.write_memory("# Memory\n- Research notes")
|
|
||||||
|
|
||||||
original = store.git.auto_commit
|
|
||||||
store.git.auto_commit = MagicMock(wraps=original)
|
|
||||||
|
|
||||||
diff_body = store.dream_content_diff()
|
|
||||||
productive = bool(diff_body)
|
|
||||||
if productive and store.git.is_initialized():
|
|
||||||
store.git.auto_commit("dream: periodic memory consolidation")
|
|
||||||
|
|
||||||
store.git.auto_commit.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestDreamContentDiff:
|
class TestDreamContentDiff:
|
||||||
"""The ground-truth signal that gates cursor advance and commit messages."""
|
"""The ground-truth signal that gates cursor advance and commit messages."""
|
||||||
|
|||||||
@ -155,6 +155,36 @@ def test_disabled_dream_cursor_only_advances_when_behind(tmp_path) -> None:
|
|||||||
assert store.get_last_dream_cursor() == 10
|
assert store.get_last_dream_cursor() == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_commit_dream_changes_skips_noop_run(tmp_path) -> None:
|
||||||
|
store = MemoryStore(tmp_path)
|
||||||
|
store.write_soul("# Soul")
|
||||||
|
store.write_memory("# Memory")
|
||||||
|
store.git.init()
|
||||||
|
store.git.auto_commit("initial")
|
||||||
|
store.git.auto_commit = MagicMock(wraps=store.git.auto_commit)
|
||||||
|
|
||||||
|
assert cli_commands._commit_dream_changes(store) is None
|
||||||
|
store.git.auto_commit.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_commit_dream_changes_commits_real_edits(tmp_path) -> None:
|
||||||
|
store = MemoryStore(tmp_path)
|
||||||
|
store.write_soul("# Soul")
|
||||||
|
store.write_memory("# Memory")
|
||||||
|
store.git.init()
|
||||||
|
store.git.auto_commit("initial")
|
||||||
|
store.write_memory("# Memory\n- Research notes")
|
||||||
|
store.git.auto_commit = MagicMock(wraps=store.git.auto_commit)
|
||||||
|
|
||||||
|
sha = cli_commands._commit_dream_changes(store)
|
||||||
|
|
||||||
|
assert sha is not None
|
||||||
|
store.git.auto_commit.assert_called_once()
|
||||||
|
message = store.git.auto_commit.call_args.args[0]
|
||||||
|
assert message.startswith("dream: periodic memory consolidation\n\n")
|
||||||
|
assert "Research notes" in message
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_paths():
|
def mock_paths():
|
||||||
"""Mock config/workspace paths for test isolation."""
|
"""Mock config/workspace paths for test isolation."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user