mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
fix(session): clear file state at deletion boundary
This commit is contained in:
@@ -377,11 +377,15 @@ class AgentLoop:
|
|||||||
|
|
||||||
self.context = ContextBuilder(workspace, timezone=timezone, disabled_skills=disabled_skills)
|
self.context = ContextBuilder(workspace, timezone=timezone, disabled_skills=disabled_skills)
|
||||||
self.sessions = session_manager or SessionManager(workspace)
|
self.sessions = session_manager or SessionManager(workspace)
|
||||||
self.sessions.set_file_cap_archiver(self.context.memory.raw_archive)
|
|
||||||
self.tools = tool_registry if tool_registry is not None else ToolRegistry()
|
|
||||||
# One file-read/write tracker per logical session. The tool registry is
|
# One file-read/write tracker per logical session. The tool registry is
|
||||||
# shared by this loop, so tools resolve the active state via contextvars.
|
# shared by this loop, so tools resolve the active state via contextvars.
|
||||||
self._file_state_store = FileStateStore(max_sessions=SESSION_CACHE_MAX_SIZE)
|
self._file_state_store = FileStateStore(max_sessions=SESSION_CACHE_MAX_SIZE)
|
||||||
|
# SessionManager owns every durable deletion entrypoint, including the
|
||||||
|
# WebUI and fork rollback paths. Observe that boundary once instead of
|
||||||
|
# duplicating cleanup in each consumer.
|
||||||
|
self.sessions.set_delete_observer(self._file_state_store.discard)
|
||||||
|
self.sessions.set_file_cap_archiver(self.context.memory.raw_archive)
|
||||||
|
self.tools = tool_registry if tool_registry is not None else ToolRegistry()
|
||||||
self._exec_session_manager = ExecSessionManager()
|
self._exec_session_manager = ExecSessionManager()
|
||||||
self.runner = AgentRunner()
|
self.runner = AgentRunner()
|
||||||
self.subagents = SubagentManager(
|
self.subagents = SubagentManager(
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ class SessionClient:
|
|||||||
|
|
||||||
def delete(self, session_key: str) -> bool:
|
def delete(self, session_key: str) -> bool:
|
||||||
"""Delete one session from disk and cache."""
|
"""Delete one session from disk and cache."""
|
||||||
self._loop.discard_session_file_state(session_key)
|
|
||||||
return self._loop.sessions.delete_session(session_key)
|
return self._loop.sessions.delete_session(session_key)
|
||||||
|
|
||||||
def flush(self) -> int:
|
def flush(self) -> int:
|
||||||
|
|||||||
@@ -1523,6 +1523,7 @@ class SessionManager:
|
|||||||
self._overflow_cache: WeakValueDictionary[str, Session] = WeakValueDictionary()
|
self._overflow_cache: WeakValueDictionary[str, Session] = WeakValueDictionary()
|
||||||
self._max_cached_sessions = SESSION_CACHE_MAX_SIZE
|
self._max_cached_sessions = SESSION_CACHE_MAX_SIZE
|
||||||
self._file_cap_archiver: Callable[..., None] | None = None
|
self._file_cap_archiver: Callable[..., None] | None = None
|
||||||
|
self._delete_observer: Callable[[str], None] | None = None
|
||||||
|
|
||||||
def _remember(self, session: Session) -> None:
|
def _remember(self, session: Session) -> None:
|
||||||
"""Keep recent sessions strongly cached without duplicating live objects."""
|
"""Keep recent sessions strongly cached without duplicating live objects."""
|
||||||
@@ -1552,6 +1553,10 @@ class SessionManager:
|
|||||||
"""Archive unconsolidated overflow whenever a session is persisted."""
|
"""Archive unconsolidated overflow whenever a session is persisted."""
|
||||||
self._file_cap_archiver = archiver
|
self._file_cap_archiver = archiver
|
||||||
|
|
||||||
|
def set_delete_observer(self, observer: Callable[[str], None]) -> None:
|
||||||
|
"""Observe explicit session deletion for process-local state cleanup."""
|
||||||
|
self._delete_observer = observer
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def safe_key(key: str) -> str:
|
def safe_key(key: str) -> str:
|
||||||
"""Public helper used by HTTP handlers to map an arbitrary key to a stable filename stem."""
|
"""Public helper used by HTTP handlers to map an arbitrary key to a stable filename stem."""
|
||||||
@@ -1684,7 +1689,10 @@ class SessionManager:
|
|||||||
def delete_session(self, key: str) -> bool:
|
def delete_session(self, key: str) -> bool:
|
||||||
"""Delete a persisted session and invalidate its cache entry."""
|
"""Delete a persisted session and invalidate its cache entry."""
|
||||||
self.invalidate(key)
|
self.invalidate(key)
|
||||||
return self._store.delete(key)
|
deleted = self._store.delete(key)
|
||||||
|
if self._delete_observer is not None:
|
||||||
|
self._delete_observer(key)
|
||||||
|
return deleted
|
||||||
|
|
||||||
def restore_sessions_to_workspace(self) -> SessionRestoreResult:
|
def restore_sessions_to_workspace(self) -> SessionRestoreResult:
|
||||||
"""Restore session files to the pre-relocation path for an explicit rollback."""
|
"""Restore session files to the pre-relocation path for an explicit rollback."""
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ def test_delete_session_returns_false_when_missing(tmp_path: Path) -> None:
|
|||||||
assert sm.delete_session("nope:none") is False
|
assert sm.delete_session("nope:none") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_session_notifies_process_local_state_observer(tmp_path: Path) -> None:
|
||||||
|
sm = _seed(tmp_path, "websocket:abc")
|
||||||
|
deleted_keys: list[str] = []
|
||||||
|
sm.set_delete_observer(deleted_keys.append)
|
||||||
|
|
||||||
|
assert sm.delete_session("websocket:abc") is True
|
||||||
|
assert sm.delete_session("websocket:missing") is False
|
||||||
|
|
||||||
|
assert deleted_keys == ["websocket:abc", "websocket:missing"]
|
||||||
|
|
||||||
|
|
||||||
def test_read_session_file_returns_metadata_and_messages(tmp_path: Path) -> None:
|
def test_read_session_file_returns_metadata_and_messages(tmp_path: Path) -> None:
|
||||||
sm = _seed(tmp_path, "telegram:abc")
|
sm = _seed(tmp_path, "telegram:abc")
|
||||||
data = sm.read_session_file("telegram:abc")
|
data = sm.read_session_file("telegram:abc")
|
||||||
|
|||||||
Reference in New Issue
Block a user