refactor(/new): background archival with guaranteed persistence

Replace fire-and-forget consolidation with archive_messages(), which
retries until the raw-dump fallback triggers — making it effectively
infallible. /new now clears the session immediately and archives in
the background. Pending archive tasks are drained on shutdown via
close_mcp() so no data is lost on process exit.
This commit is contained in:
Xubin Ren
2026-03-16 08:33:03 +00:00
committed by Xubin Ren
parent 9820c87537
commit b29275a1d2
3 changed files with 56 additions and 33 deletions

View File

@@ -506,7 +506,7 @@ class TestNewCommandArchival:
@pytest.mark.asyncio
async def test_new_clears_session_immediately_even_if_archive_fails(self, tmp_path: Path) -> None:
"""/new clears session immediately, archive failure only logs warning."""
"""/new clears session immediately; archive_messages retries until raw dump."""
from nanobot.bus.events import InboundMessage
loop = self._make_loop(tmp_path)
@@ -516,7 +516,11 @@ class TestNewCommandArchival:
session.add_message("assistant", f"resp{i}")
loop.sessions.save(session)
call_count = 0
async def _failing_consolidate(_messages) -> bool:
nonlocal call_count
call_count += 1
return False
loop.memory_consolidator.consolidate_messages = _failing_consolidate # type: ignore[method-assign]
@@ -524,14 +528,15 @@ class TestNewCommandArchival:
new_msg = InboundMessage(channel="cli", sender_id="user", chat_id="test", content="/new")
response = await loop._process_message(new_msg)
# /new returns immediately with success message
assert response is not None
assert "new session started" in response.content.lower()
# Session is cleared immediately, even though archive will fail in background
session_after = loop.sessions.get_or_create("cli:test")
assert len(session_after.messages) == 0
await loop.close_mcp()
assert call_count == 3 # retried up to raw-archive threshold
@pytest.mark.asyncio
async def test_new_archives_only_unconsolidated_messages(self, tmp_path: Path) -> None:
from nanobot.bus.events import InboundMessage
@@ -545,12 +550,10 @@ class TestNewCommandArchival:
loop.sessions.save(session)
archived_count = -1
archive_done = asyncio.Event()
async def _fake_consolidate(messages) -> bool:
nonlocal archived_count
archived_count = len(messages)
archive_done.set()
return True
loop.memory_consolidator.consolidate_messages = _fake_consolidate # type: ignore[method-assign]
@@ -561,8 +564,7 @@ class TestNewCommandArchival:
assert response is not None
assert "new session started" in response.content.lower()
# Wait for background archival to complete
await asyncio.wait_for(archive_done.wait(), timeout=1.0)
await loop.close_mcp()
assert archived_count == 3
@pytest.mark.asyncio
@@ -587,3 +589,31 @@ class TestNewCommandArchival:
assert response is not None
assert "new session started" in response.content.lower()
assert loop.sessions.get_or_create("cli:test").messages == []
@pytest.mark.asyncio
async def test_close_mcp_drains_pending_archives(self, tmp_path: Path) -> None:
"""close_mcp waits for background archive tasks to complete."""
from nanobot.bus.events import InboundMessage
loop = self._make_loop(tmp_path)
session = loop.sessions.get_or_create("cli:test")
for i in range(3):
session.add_message("user", f"msg{i}")
session.add_message("assistant", f"resp{i}")
loop.sessions.save(session)
archived = asyncio.Event()
async def _slow_consolidate(_messages) -> bool:
await asyncio.sleep(0.1)
archived.set()
return True
loop.memory_consolidator.consolidate_messages = _slow_consolidate # type: ignore[method-assign]
new_msg = InboundMessage(channel="cli", sender_id="user", chat_id="test", content="/new")
await loop._process_message(new_msg)
assert not archived.is_set()
await loop.close_mcp()
assert archived.is_set()