mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-20 08:32:25 +00:00
fix(dream): restore cursor with memory state
Track the Dream cursor in memory versioning so restores do not skip history after rolling back Dream commits. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ac18a8baad
commit
44a341335a
@ -8,19 +8,24 @@ import os
|
|||||||
import re
|
import re
|
||||||
import weakref
|
import weakref
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import tiktoken
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Iterator
|
from typing import TYPE_CHECKING, Any, Callable, Iterator
|
||||||
|
|
||||||
|
import tiktoken
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from nanobot.utils.prompt_templates import render_template
|
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||||
from nanobot.utils.helpers import ensure_dir, estimate_message_tokens, estimate_prompt_tokens_chain, strip_think, truncate_text
|
|
||||||
|
|
||||||
from nanobot.agent.runner import AgentRunSpec, AgentRunner
|
|
||||||
from nanobot.agent.tools.registry import ToolRegistry
|
from nanobot.agent.tools.registry import ToolRegistry
|
||||||
from nanobot.utils.gitstore import GitStore
|
from nanobot.utils.gitstore import GitStore
|
||||||
|
from nanobot.utils.helpers import (
|
||||||
|
ensure_dir,
|
||||||
|
estimate_message_tokens,
|
||||||
|
estimate_prompt_tokens_chain,
|
||||||
|
strip_think,
|
||||||
|
truncate_text,
|
||||||
|
)
|
||||||
|
from nanobot.utils.prompt_templates import render_template
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nanobot.providers.base import LLMProvider
|
from nanobot.providers.base import LLMProvider
|
||||||
@ -55,7 +60,7 @@ class MemoryStore:
|
|||||||
self._corruption_logged = False # rate-limit non-int cursor warning
|
self._corruption_logged = False # rate-limit non-int cursor warning
|
||||||
self._oversize_logged = False # rate-limit oversized-entry warning
|
self._oversize_logged = False # rate-limit oversized-entry warning
|
||||||
self._git = GitStore(workspace, tracked_files=[
|
self._git = GitStore(workspace, tracked_files=[
|
||||||
"SOUL.md", "USER.md", "memory/MEMORY.md",
|
"SOUL.md", "USER.md", "memory/MEMORY.md", "memory/.dream_cursor",
|
||||||
])
|
])
|
||||||
self._maybe_migrate_legacy_history()
|
self._maybe_migrate_legacy_history()
|
||||||
|
|
||||||
@ -350,7 +355,7 @@ class MemoryStore:
|
|||||||
read_size = min(size, 4096)
|
read_size = min(size, 4096)
|
||||||
f.seek(size - read_size)
|
f.seek(size - read_size)
|
||||||
data = f.read().decode("utf-8")
|
data = f.read().decode("utf-8")
|
||||||
lines = [l for l in data.split("\n") if l.strip()]
|
lines = [line for line in data.split("\n") if line.strip()]
|
||||||
if not lines:
|
if not lines:
|
||||||
return None
|
return None
|
||||||
return json.loads(lines[-1])
|
return json.loads(lines[-1])
|
||||||
@ -780,7 +785,7 @@ class Dream:
|
|||||||
|
|
||||||
from nanobot.agent.skills import BUILTIN_SKILLS_DIR
|
from nanobot.agent.skills import BUILTIN_SKILLS_DIR
|
||||||
|
|
||||||
_DESC_RE = _re.compile(r"^description:\s*(.+)$", _re.MULTILINE | _re.IGNORECASE)
|
desc_re = _re.compile(r"^description:\s*(.+)$", _re.MULTILINE | _re.IGNORECASE)
|
||||||
entries: dict[str, str] = {}
|
entries: dict[str, str] = {}
|
||||||
for base in (self.store.workspace / "skills", BUILTIN_SKILLS_DIR):
|
for base in (self.store.workspace / "skills", BUILTIN_SKILLS_DIR):
|
||||||
if not base.exists():
|
if not base.exists():
|
||||||
@ -795,7 +800,7 @@ class Dream:
|
|||||||
if d.name in entries and base == BUILTIN_SKILLS_DIR:
|
if d.name in entries and base == BUILTIN_SKILLS_DIR:
|
||||||
continue
|
continue
|
||||||
content = skill_md.read_text(encoding="utf-8")[:500]
|
content = skill_md.read_text(encoding="utf-8")[:500]
|
||||||
m = _DESC_RE.search(content)
|
m = desc_re.search(content)
|
||||||
desc = m.group(1).strip() if m else "(no description)"
|
desc = m.group(1).strip() if m else "(no description)"
|
||||||
entries[d.name] = desc
|
entries[d.name] = desc
|
||||||
return [f"{name} — {desc}" for name, desc in sorted(entries.items())]
|
return [f"{name} — {desc}" for name, desc in sorted(entries.items())]
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent.memory import MemoryStore, _HISTORY_ENTRY_HARD_CAP
|
from nanobot.agent.memory import _HISTORY_ENTRY_HARD_CAP, MemoryStore
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -241,6 +241,26 @@ class TestDreamCursor:
|
|||||||
store2 = MemoryStore(store.workspace)
|
store2 = MemoryStore(store.workspace)
|
||||||
assert store2.get_last_dream_cursor() == 3
|
assert store2.get_last_dream_cursor() == 3
|
||||||
|
|
||||||
|
def test_git_restore_rolls_back_dream_cursor(self, tmp_path):
|
||||||
|
store = MemoryStore(tmp_path)
|
||||||
|
store.write_memory("before")
|
||||||
|
store.set_last_dream_cursor(1)
|
||||||
|
assert store.git.init() is True
|
||||||
|
|
||||||
|
store.write_memory("after")
|
||||||
|
store.set_last_dream_cursor(2)
|
||||||
|
dream_sha = store.git.auto_commit("dream: update")
|
||||||
|
assert dream_sha is not None
|
||||||
|
|
||||||
|
store.write_memory("newer")
|
||||||
|
store.set_last_dream_cursor(3)
|
||||||
|
|
||||||
|
restore_sha = store.git.revert(dream_sha)
|
||||||
|
|
||||||
|
assert restore_sha is not None
|
||||||
|
assert store.read_memory() == "before"
|
||||||
|
assert store.get_last_dream_cursor() == 1
|
||||||
|
|
||||||
|
|
||||||
class TestLegacyHistoryMigration:
|
class TestLegacyHistoryMigration:
|
||||||
def test_read_unprocessed_history_handles_entries_without_cursor(self, store):
|
def test_read_unprocessed_history_handles_entries_without_cursor(self, store):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user