fix(session): clear file state at deletion boundary

This commit is contained in:
Xubin Ren
2026-08-15 23:49:20 +08:00
parent 42afebb0cb
commit 2f19068eb0
4 changed files with 26 additions and 4 deletions
+6 -2
View File
@@ -377,11 +377,15 @@ class AgentLoop:
self.context = ContextBuilder(workspace, timezone=timezone, disabled_skills=disabled_skills)
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
# shared by this loop, so tools resolve the active state via contextvars.
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.runner = AgentRunner()
self.subagents = SubagentManager(
-1
View File
@@ -146,7 +146,6 @@ class SessionClient:
def delete(self, session_key: str) -> bool:
"""Delete one session from disk and cache."""
self._loop.discard_session_file_state(session_key)
return self._loop.sessions.delete_session(session_key)
def flush(self) -> int:
+9 -1
View File
@@ -1523,6 +1523,7 @@ class SessionManager:
self._overflow_cache: WeakValueDictionary[str, Session] = WeakValueDictionary()
self._max_cached_sessions = SESSION_CACHE_MAX_SIZE
self._file_cap_archiver: Callable[..., None] | None = None
self._delete_observer: Callable[[str], None] | None = None
def _remember(self, session: Session) -> None:
"""Keep recent sessions strongly cached without duplicating live objects."""
@@ -1552,6 +1553,10 @@ class SessionManager:
"""Archive unconsolidated overflow whenever a session is persisted."""
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
def safe_key(key: str) -> str:
"""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:
"""Delete a persisted session and invalidate its cache entry."""
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:
"""Restore session files to the pre-relocation path for an explicit rollback."""
+11
View File
@@ -34,6 +34,17 @@ def test_delete_session_returns_false_when_missing(tmp_path: Path) -> None:
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:
sm = _seed(tmp_path, "telegram:abc")
data = sm.read_session_file("telegram:abc")