diff --git a/nanobot/agent/auto_compact.py b/nanobot/agent/auto_compact.py index 171f5f55a..f30feac17 100644 --- a/nanobot/agent/auto_compact.py +++ b/nanobot/agent/auto_compact.py @@ -53,9 +53,7 @@ class AutoCompact: return n = len(msgs) last_active = session.updated_at - await self.consolidator.archive(msgs) - entry = self.consolidator.get_last_history_entry() - summary = (entry or {}).get("content", "") + summary = await self.consolidator.archive(msgs) or "" if summary and summary != "(nothing)": self._summaries[key] = (summary, last_active) session.metadata["_last_summary"] = {"text": summary, "last_active": last_active.isoformat()} @@ -71,6 +69,8 @@ class AutoCompact: if key in self._archiving or self._is_expired(session.updated_at): logger.info("Auto-compact: reloading session {} (archiving={})", key, key in self._archiving) session = self.sessions.get_or_create(key) + # Hot path: summary from in-memory dict (process hasn't restarted). + # Also clean metadata copy so stale _last_summary never leaks to disk. entry = self._summaries.pop(key, None) if entry: session.metadata.pop("_last_summary", None) diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index e9662ff2c..04d988ee5 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -374,10 +374,6 @@ class Consolidator: weakref.WeakValueDictionary() ) - def get_last_history_entry(self) -> dict[str, Any] | None: - """Return the most recent entry from history.jsonl.""" - return self.store._read_last_entry() - def get_lock(self, session_key: str) -> asyncio.Lock: """Return the shared consolidation lock for one session.""" return self._locks.setdefault(session_key, asyncio.Lock()) @@ -437,13 +433,13 @@ class Consolidator: self._get_tool_definitions(), ) - async def archive(self, messages: list[dict]) -> bool: + async def archive(self, messages: list[dict]) -> str | None: """Summarize messages via LLM and append to history.jsonl. - Returns True on success (or degraded success), False if nothing to do. + Returns the summary text on success, None if nothing to archive. """ if not messages: - return False + return None try: formatted = MemoryStore._format_messages(messages) response = await self.provider.chat_with_retry( @@ -463,11 +459,11 @@ class Consolidator: ) summary = response.content or "[no summary]" self.store.append_history(summary) - return True + return summary except Exception: logger.warning("Consolidation LLM call failed, raw-dumping to history") self.store.raw_archive(messages) - return True + return None async def maybe_consolidate_by_tokens(self, session: Session) -> None: """Loop: archive old messages until prompt fits within safe budget. diff --git a/tests/agent/test_auto_compact.py b/tests/agent/test_auto_compact.py index 8b26254e9..39792e290 100644 --- a/tests/agent/test_auto_compact.py +++ b/tests/agent/test_auto_compact.py @@ -101,7 +101,7 @@ class TestAutoCompact: loop.sessions.save(s2) async def _fake_archive(messages): - return True + return "Summary." loop.consolidator.archive = _fake_archive loop.auto_compact.check_expired(loop._schedule_background) @@ -126,7 +126,7 @@ class TestAutoCompact: async def _fake_archive(messages): archived_messages.extend(messages) - return True + return "Summary." loop.consolidator.archive = _fake_archive @@ -147,12 +147,9 @@ class TestAutoCompact: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "User said hello." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "User said hello.", - } await loop.auto_compact._archive("cli:test") @@ -174,7 +171,7 @@ class TestAutoCompact: async def _fake_archive(messages): nonlocal archive_called archive_called = True - return True + return "Summary." loop.consolidator.archive = _fake_archive @@ -201,7 +198,7 @@ class TestAutoCompact: async def _fake_archive(messages): nonlocal archived_count archived_count = len(messages) - return True + return "Summary." loop.consolidator.archive = _fake_archive @@ -243,12 +240,9 @@ class TestAutoCompactIdleDetection: async def _fake_archive(messages): archived_messages.extend(messages) - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } # Simulate proactive archive completing before message arrives await loop.auto_compact._archive("cli:test") @@ -311,7 +305,7 @@ class TestAutoCompactIdleDetection: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "Summary." loop.consolidator.archive = _fake_archive @@ -340,12 +334,9 @@ class TestAutoCompactSystemMessages: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } # Simulate proactive archive completing before system message arrives await loop.auto_compact._archive("cli:test") @@ -428,12 +419,9 @@ class TestAutoCompactEdgeCases: async def _fake_archive(messages): archived_messages.extend(messages) - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } # Simulate proactive archive completing before message arrives await loop.auto_compact._archive("cli:test") @@ -518,12 +506,9 @@ class TestAutoCompactIntegration: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } # Simulate proactive archive completing before message arrives await loop.auto_compact._archive("cli:test") @@ -586,12 +571,9 @@ class TestProactiveAutoCompact: async def _fake_archive(messages): archived_messages.extend(messages) - return True + return "User chatted about old things." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "User chatted about old things.", - } await self._run_check_expired(loop) @@ -635,7 +617,7 @@ class TestProactiveAutoCompact: archive_count += 1 started.set() await block_forever.wait() - return True + return "Summary." loop.consolidator.archive = _slow_archive @@ -688,7 +670,7 @@ class TestProactiveAutoCompact: async def _fake_archive(messages): nonlocal archive_called archive_called = True - return True + return "Summary." loop.consolidator.archive = _fake_archive @@ -712,12 +694,9 @@ class TestProactiveAutoCompact: async def _fake_archive(messages): nonlocal archive_count archive_count += 1 - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } # First tick: archives the session await self._run_check_expired(loop) @@ -741,7 +720,7 @@ class TestProactiveAutoCompact: async def _fake_archive(messages): nonlocal archive_count archive_count += 1 - return True + return "Summary." loop.consolidator.archive = _fake_archive @@ -769,12 +748,9 @@ class TestProactiveAutoCompact: async def _fake_archive(messages): nonlocal archive_count archive_count += 1 - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } # First compact cycle await loop.auto_compact._archive("cli:test") @@ -810,12 +786,9 @@ class TestSummaryPersistence: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "User said hello." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "User said hello.", - } await loop.auto_compact._archive("cli:test") @@ -839,12 +812,9 @@ class TestSummaryPersistence: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "User said hello." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "User said hello.", - } # Archive await loop.auto_compact._archive("cli:test") @@ -874,12 +844,9 @@ class TestSummaryPersistence: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } await loop.auto_compact._archive("cli:test") @@ -908,12 +875,9 @@ class TestSummaryPersistence: loop.sessions.save(session) async def _fake_archive(messages): - return True + return "Summary." loop.consolidator.archive = _fake_archive - loop.consolidator.get_last_history_entry = lambda: { - "cursor": 1, "timestamp": "2026-01-01 00:00", "content": "Summary.", - } await loop.auto_compact._archive("cli:test") diff --git a/tests/agent/test_consolidator.py b/tests/agent/test_consolidator.py index b7989d9dd..28587e1b4 100644 --- a/tests/agent/test_consolidator.py +++ b/tests/agent/test_consolidator.py @@ -46,7 +46,7 @@ class TestConsolidatorSummarize: {"role": "assistant", "content": "Done, fixed the race condition."}, ] result = await consolidator.archive(messages) - assert result is True + assert result == "User fixed a bug in the auth module." entries = store.read_unprocessed_history(since_cursor=0) assert len(entries) == 1 @@ -55,14 +55,14 @@ class TestConsolidatorSummarize: mock_provider.chat_with_retry.side_effect = Exception("API error") messages = [{"role": "user", "content": "hello"}] result = await consolidator.archive(messages) - assert result is True # always succeeds + assert result is None # no summary on raw dump fallback entries = store.read_unprocessed_history(since_cursor=0) assert len(entries) == 1 assert "[RAW]" in entries[0]["content"] async def test_summarize_skips_empty_messages(self, consolidator): result = await consolidator.archive([]) - assert result is False + assert result is None class TestConsolidatorTokenBudget: