fix(memory): prune encoded Dream sessions
This commit is contained in:
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Any, Callable, Iterator
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from nanobot.runtime_context import public_history_messages
|
from nanobot.runtime_context import public_history_messages
|
||||||
from nanobot.session.manager import Session
|
from nanobot.session.manager import Session, SessionManager
|
||||||
from nanobot.utils.gitstore import GitStore
|
from nanobot.utils.gitstore import GitStore
|
||||||
from nanobot.utils.helpers import (
|
from nanobot.utils.helpers import (
|
||||||
ensure_dir,
|
ensure_dir,
|
||||||
@@ -31,7 +31,6 @@ from nanobot.utils.helpers import (
|
|||||||
from nanobot.utils.prompt_templates import render_template
|
from nanobot.utils.prompt_templates import render_template
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nanobot.session.manager import SessionManager
|
|
||||||
from nanobot.utils.llm_runtime import LLMRuntime
|
from nanobot.utils.llm_runtime import LLMRuntime
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -705,12 +704,15 @@ class MemoryStore:
|
|||||||
def prune_dream_sessions(sessions_dir: Path, *, keep: int = 10) -> None:
|
def prune_dream_sessions(sessions_dir: Path, *, keep: int = 10) -> None:
|
||||||
"""Remove the oldest Dream session files, keeping only the N most recent.
|
"""Remove the oldest Dream session files, keeping only the N most recent.
|
||||||
|
|
||||||
Only files matching ``dream_*.jsonl`` are considered. Non-dream session
|
Only current base64url-encoded Dream session keys are considered.
|
||||||
files are never touched.
|
Non-dream session files are never touched.
|
||||||
"""
|
"""
|
||||||
dream_files = sorted(
|
dream_files = []
|
||||||
sessions_dir.glob("dream_*.jsonl"), key=lambda p: p.stat().st_mtime,
|
for path in sessions_dir.glob("*.jsonl"):
|
||||||
)
|
decoded_key = SessionManager._decode_storage_key(path.stem)
|
||||||
|
if decoded_key is not None and decoded_key.startswith("dream:"):
|
||||||
|
dream_files.append(path)
|
||||||
|
dream_files.sort(key=lambda p: p.stat().st_mtime)
|
||||||
if len(dream_files) <= keep:
|
if len(dream_files) <= keep:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"""Tests for Dream session key generation and rotation."""
|
"""Tests for Dream session key generation and rotation."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from nanobot.agent.memory import MemoryStore
|
from nanobot.agent.memory import MemoryStore
|
||||||
|
from nanobot.session.manager import SessionManager
|
||||||
|
|
||||||
|
|
||||||
class TestDreamSessionKey:
|
class TestDreamSessionKey:
|
||||||
@@ -31,11 +33,11 @@ class TestPruneDreamSessions:
|
|||||||
sessions_dir.mkdir()
|
sessions_dir.mkdir()
|
||||||
|
|
||||||
base_time = time.time() - 100
|
base_time = time.time() - 100
|
||||||
|
dream_paths = []
|
||||||
|
|
||||||
for i in range(15):
|
for i in range(15):
|
||||||
key = f"dream:20260528-{100000 + i:06d}"
|
key = f"dream:20260528-{100000 + i:06d}"
|
||||||
safe_key = key.replace(":", "_")
|
path = sessions_dir / f"{SessionManager._storage_key(key)}.jsonl"
|
||||||
path = sessions_dir / f"{safe_key}.jsonl"
|
|
||||||
path.write_text(
|
path.write_text(
|
||||||
f'{{"_type": "metadata", "key": "{key}", '
|
f'{{"_type": "metadata", "key": "{key}", '
|
||||||
f'"created_at": "2026-05-28T10:00:{i:02d}", '
|
f'"created_at": "2026-05-28T10:00:{i:02d}", '
|
||||||
@@ -43,29 +45,57 @@ class TestPruneDreamSessions:
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
os.utime(path, (base_time + i, base_time + i))
|
os.utime(path, (base_time + i, base_time + i))
|
||||||
|
dream_paths.append(path)
|
||||||
|
|
||||||
normal_path = sessions_dir / "telegram_123.jsonl"
|
normal_path = sessions_dir / "telegram_123.jsonl"
|
||||||
normal_path.write_text('{"_type": "metadata"}\n', encoding="utf-8")
|
normal_path.write_text('{"_type": "metadata"}\n', encoding="utf-8")
|
||||||
|
|
||||||
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
|
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
|
||||||
|
|
||||||
dream_files = sorted(sessions_dir.glob("dream_*.jsonl"))
|
assert [path.exists() for path in dream_paths] == [False] * 5 + [True] * 10
|
||||||
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()
|
assert normal_path.exists()
|
||||||
|
|
||||||
|
def test_ignores_legacy_dream_filenames(self, tmp_path):
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
sessions_dir = tmp_path / "sessions"
|
||||||
|
sessions_dir.mkdir()
|
||||||
|
base_time = time.time() - 100
|
||||||
|
current_paths = []
|
||||||
|
|
||||||
|
for i in range(2):
|
||||||
|
key = f"dream:20260713-{100000 + i:06d}"
|
||||||
|
path = sessions_dir / f"{SessionManager._storage_key(key)}.jsonl"
|
||||||
|
path.write_text(
|
||||||
|
f'{{"_type": "metadata", "key": "{key}"}}\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
os.utime(path, (base_time + i, base_time + i))
|
||||||
|
current_paths.append(path)
|
||||||
|
|
||||||
|
legacy_path = sessions_dir / "dream_20260713-095959.jsonl"
|
||||||
|
legacy_path.write_text(
|
||||||
|
'{"_type": "metadata", "key": "dream:20260713-095959"}\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
os.utime(legacy_path, (base_time - 1, base_time - 1))
|
||||||
|
|
||||||
|
MemoryStore.prune_dream_sessions(sessions_dir, keep=1)
|
||||||
|
|
||||||
|
assert [path.exists() for path in current_paths] == [False, True]
|
||||||
|
assert legacy_path.exists()
|
||||||
|
|
||||||
def test_noop_when_under_limit(self, tmp_path):
|
def test_noop_when_under_limit(self, tmp_path):
|
||||||
sessions_dir = tmp_path / "sessions"
|
sessions_dir = tmp_path / "sessions"
|
||||||
sessions_dir.mkdir()
|
sessions_dir.mkdir()
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
key = f"dream:20260528-{100000 + i:06d}"
|
key = f"dream:20260528-{100000 + i:06d}"
|
||||||
safe_key = key.replace(":", "_")
|
path = sessions_dir / f"{SessionManager._storage_key(key)}.jsonl"
|
||||||
(sessions_dir / f"{safe_key}.jsonl").write_text("{}", encoding="utf-8")
|
path.write_text("{}", encoding="utf-8")
|
||||||
|
|
||||||
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
|
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
|
||||||
assert len(list(sessions_dir.glob("dream_*.jsonl"))) == 3
|
assert len(list(sessions_dir.glob("*.jsonl"))) == 3
|
||||||
|
|
||||||
def test_empty_dir_noop(self, tmp_path):
|
def test_empty_dir_noop(self, tmp_path):
|
||||||
sessions_dir = tmp_path / "sessions"
|
sessions_dir = tmp_path / "sessions"
|
||||||
|
|||||||
Reference in New Issue
Block a user