fix(agent): bound per-session file state

This commit is contained in:
yu-xin-c
2026-08-15 23:49:20 +08:00
committed by Xubin Ren
parent ecef2b055d
commit 42afebb0cb
8 changed files with 73 additions and 7 deletions
+2
View File
@@ -131,6 +131,7 @@ async def test_session_discard_control_cancels_active_turn(tmp_path, monkeypatch
terminate_exec_sessions,
)
key = "websocket:transient-cancelled"
previous_file_state = loop._file_state_store.for_session(key)
loop.sessions.get_or_create_transient(
key,
disabled_tools={"create_goal", "update_goal", "spawn", "cron"},
@@ -157,6 +158,7 @@ async def test_session_discard_control_cancels_active_turn(tmp_path, monkeypatch
await asyncio.wait_for(active_task, timeout=2)
await asyncio.wait_for(wait_for_discard(key), timeout=2)
assert loop.sessions.get_cached(key) is None
assert loop._file_state_store.for_session(key) is not previous_file_state
terminate_exec_sessions.assert_awaited_once_with(key)
loop.stop()
+11
View File
@@ -20,6 +20,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from nanobot.agent.loop import AgentLoop
from nanobot.agent.tools.file_state import FileStateStore
from nanobot.bus.events import InboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.command.builtin import cmd_new, register_builtin_commands
@@ -250,10 +251,16 @@ class TestCmdNewUnifiedSession:
# asyncio.create_task(). Mirror that exactly so the coroutine is consumed
# and no RuntimeWarning is emitted.
admitted_runtime = MagicMock(name="admitted_runtime")
file_state_store = FileStateStore()
previous_file_state = file_state_store.for_session("unified:default")
tracked_file = tmp_path / "tracked.txt"
tracked_file.write_text("tracked", encoding="utf-8")
previous_file_state.record_read(tracked_file)
loop = SimpleNamespace(
sessions=sessions,
consolidator=SimpleNamespace(archive=AsyncMock(return_value=True)),
_cancel_active_tasks=AsyncMock(return_value=0),
discard_session_file_state=file_state_store.discard,
llm_runtime=MagicMock(return_value=MagicMock()),
schedule_background=lambda coro: asyncio.ensure_future(coro),
)
@@ -278,6 +285,9 @@ class TestCmdNewUnifiedSession:
sessions.invalidate("unified:default")
reloaded = sessions.get_or_create("unified:default")
assert reloaded.messages == []
reset_file_state = file_state_store.for_session("unified:default")
assert reset_file_state is not previous_file_state
assert reset_file_state.is_unchanged(tracked_file) is False
loop.consolidator.archive.assert_called_once_with(
expected_snapshot,
runtime=admitted_runtime,
@@ -302,6 +312,7 @@ class TestCmdNewUnifiedSession:
sessions=sessions,
consolidator=SimpleNamespace(archive=AsyncMock(return_value=True)),
_cancel_active_tasks=AsyncMock(return_value=0),
discard_session_file_state=MagicMock(),
runtime_for_session=MagicMock(return_value=MagicMock()),
schedule_background=lambda coro: asyncio.ensure_future(coro),
)
+5
View File
@@ -1500,10 +1500,15 @@ async def test_session_helpers_get_list_export_clear_delete_flush(tmp_path):
exported.messages[0]["content"] = "mutated copy"
assert bot.sessions.get("sdk:first").messages[0]["content"] == "hello"
state_before_clear = bot._loop._file_state_store.for_session("sdk:first")
cleared = bot.sessions.clear("sdk:first")
assert cleared.messages == []
state_after_clear = bot._loop._file_state_store.for_session("sdk:first")
assert state_after_clear is not state_before_clear
assert bot.sessions.flush() >= 1
state_before_delete = state_after_clear
assert bot.sessions.delete("sdk:first") is True
assert bot._loop._file_state_store.for_session("sdk:first") is not state_before_delete
assert bot.sessions.get("sdk:first") is None
+29
View File
@@ -0,0 +1,29 @@
import pytest
from nanobot.agent.tools.file_state import FileStateStore
def test_file_state_store_evicts_least_recently_used_session() -> None:
store = FileStateStore(max_sessions=2)
first = store.for_session("first")
second = store.for_session("second")
assert store.for_session("first") is first
store.for_session("third")
assert store.for_session("first") is first
assert store.for_session("second") is not second
def test_file_state_store_discards_reset_session() -> None:
store = FileStateStore()
previous = store.for_session("websocket:chat")
store.discard("websocket:chat")
assert store.for_session("websocket:chat") is not previous
def test_file_state_store_requires_positive_capacity() -> None:
with pytest.raises(ValueError, match="max_sessions must be positive"):
FileStateStore(max_sessions=0)