diff --git a/nanobot/agent/autocompact.py b/nanobot/agent/autocompact.py index fa4af0efb..eabd86155 100644 --- a/nanobot/agent/autocompact.py +++ b/nanobot/agent/autocompact.py @@ -89,7 +89,6 @@ class AutoCompact: if summary and summary != "(nothing)": self._summaries[key] = (summary, last_active) session.metadata["_last_summary"] = {"text": summary, "last_active": last_active.isoformat()} - session.metadata.pop("_last_summary_used", None) session.messages = kept_msgs session.last_consolidated = 0 session.updated_at = datetime.now() @@ -112,15 +111,13 @@ class AutoCompact: 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["_last_summary_used"] = True + session.metadata.pop("_last_summary", None) return session, self._format_summary(entry[0], entry[1]) - # Cold path: summary persisted in session metadata (process restarted). - # Keep it in metadata so it survives restarts; only inject once per - # turn via the _last_summary_used sentinel. - meta = session.metadata.get("_last_summary") - if meta and not session.metadata.get("_last_summary_used"): - session.metadata["_last_summary_used"] = True + if "_last_summary" in session.metadata: + meta = session.metadata.pop("_last_summary") + self.sessions.save(session) return session, self._format_summary(meta["text"], datetime.fromisoformat(meta["last_active"])) return session, None diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index aaec64e7e..8eaf06daf 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -585,7 +585,6 @@ class Consolidator: "text": summary, "last_active": session.updated_at.isoformat(), } - session.metadata.pop("_last_summary_used", None) self.sessions.save(session) def estimate_session_prompt_tokens( diff --git a/tests/agent/test_auto_compact.py b/tests/agent/test_auto_compact.py index 963da2f36..ecef55044 100644 --- a/tests/agent/test_auto_compact.py +++ b/tests/agent/test_auto_compact.py @@ -1021,15 +1021,13 @@ class TestSummaryPersistence: assert summary is not None assert "User said hello." in summary assert "Inactive for" in summary - # Metadata persists so the summary survives restarts; _last_summary_used - # sentinel prevents duplicate injection within the same turn. - assert "_last_summary" in reloaded.metadata - assert reloaded.metadata.get("_last_summary_used") is True + # Metadata should be cleaned up after consumption + assert "_last_summary" not in reloaded.metadata await loop.close_mcp() @pytest.mark.asyncio async def test_metadata_cleanup_no_leak(self, tmp_path): - """_last_summary persists in metadata for restart survival; _last_summary_used sentinel prevents duplicate injection.""" + """_last_summary should be removed from metadata after being consumed.""" loop = _make_loop(tmp_path, session_ttl_minutes=15) session = loop.sessions.get_or_create("cli:test") _add_turns(session, 6, prefix="hello") @@ -1052,13 +1050,10 @@ class TestSummaryPersistence: _, summary = loop.auto_compact.prepare_session(reloaded, "cli:test") assert summary is not None - # Second call: no summary (already consumed this turn) + # Second call: no summary (already consumed) _, summary2 = loop.auto_compact.prepare_session(reloaded, "cli:test") assert summary2 is None - # _last_summary stays in metadata for restart survival; - # _last_summary_used sentinel prevents duplicate injection. - assert "_last_summary" in reloaded.metadata - assert reloaded.metadata.get("_last_summary_used") is True + assert "_last_summary" not in reloaded.metadata await loop.close_mcp() @pytest.mark.asyncio @@ -1086,8 +1081,6 @@ class TestSummaryPersistence: # In-memory path is taken (no restart) _, summary = loop.auto_compact.prepare_session(reloaded, "cli:test") assert summary is not None - # _last_summary stays in metadata for restart survival; - # _last_summary_used sentinel prevents duplicate injection. - assert "_last_summary" in reloaded.metadata - assert reloaded.metadata.get("_last_summary_used") is True + # Metadata should also be cleaned up + assert "_last_summary" not in reloaded.metadata await loop.close_mcp() diff --git a/tests/agent/test_loop_consolidation_tokens.py b/tests/agent/test_loop_consolidation_tokens.py index ddbafd23d..aeb67d8b3 100644 --- a/tests/agent/test_loop_consolidation_tokens.py +++ b/tests/agent/test_loop_consolidation_tokens.py @@ -190,10 +190,7 @@ async def test_consolidation_persists_summary_for_next_prepare_session(tmp_path, reloaded, pending = loop.auto_compact.prepare_session(reloaded, "cli:test") assert pending is not None assert "User discussed project status." in pending - # _last_summary persists for restart survival; _last_summary_used prevents - # duplicate injection within the same turn. - assert "_last_summary" in reloaded.metadata - assert reloaded.metadata.get("_last_summary_used") is True + assert "_last_summary" not in reloaded.metadata @pytest.mark.asyncio