From eb0ff3ad1d1250a7529eabc30314a53c2e1c5085 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Mon, 18 May 2026 01:01:34 +0800 Subject: [PATCH] fix(memory): refresh session before empty guard --- nanobot/agent/memory.py | 4 +++- tests/agent/test_consolidator.py | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index b7a325a02..ffc9c5f0e 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -678,7 +678,7 @@ class Consolidator: The budget reserves space for completion tokens and a safety buffer so the LLM request never exceeds the context window. """ - if not session.messages or self.context_window_tokens <= 0: + if self.context_window_tokens <= 0: return lock = self.get_lock(session.key) @@ -687,6 +687,8 @@ class Consolidator: fresh = self.sessions.get_or_create(session.key) if fresh is not session: session = fresh + if not session.messages: + return budget = self._input_token_budget target = int(budget * self.consolidation_ratio) diff --git a/tests/agent/test_consolidator.py b/tests/agent/test_consolidator.py index 159ec01d1..1fa05d3c8 100644 --- a/tests/agent/test_consolidator.py +++ b/tests/agent/test_consolidator.py @@ -477,6 +477,47 @@ class TestCompactIdleSession: class TestConsolidatorSessionRefresh: """Background consolidation must detect stale session references.""" + @pytest.mark.asyncio + async def test_reloads_before_empty_session_guard(self, tmp_path): + """A stale empty reference must not skip a non-empty cached session.""" + from nanobot.agent.memory import Consolidator, MemoryStore + from nanobot.session.manager import Session, SessionManager + + store = MemoryStore(tmp_path) + provider = MagicMock() + provider.chat_with_retry = AsyncMock( + return_value=MagicMock(content="summary", finish_reason="stop") + ) + provider.generation.max_tokens = 4096 + provider.estimate_prompt_tokens = MagicMock(return_value=(10, "test")) + sessions = SessionManager(tmp_path) + consolidator = Consolidator( + store=store, + provider=provider, + model="test-model", + sessions=sessions, + context_window_tokens=128_000, + build_messages=MagicMock(return_value=[]), + get_tool_definitions=MagicMock(return_value=[]), + ) + + fresh = sessions.get_or_create("cli:test") + fresh.add_message("user", "fresh message") + sessions.save(fresh) + stale_empty = Session(key="cli:test") + + seen: dict[str, Session] = {} + + def estimate(session: Session): + seen["session"] = session + return 10, "test" + + consolidator.estimate_session_prompt_tokens = MagicMock(side_effect=estimate) + + await consolidator.maybe_consolidate_by_tokens(stale_empty) + + assert seen["session"] is fresh + @pytest.mark.asyncio async def test_reloads_stale_session_after_compact(self, tmp_path): """After compact_idle_session replaces the session, a concurrent