mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
parent
15de6be0af
commit
464f71b488
@ -507,6 +507,39 @@ class SessionManager:
|
|||||||
return None
|
return None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _resolve_session_path(self, key: str, *, migrate: bool = False) -> Path | None:
|
||||||
|
"""Resolve a session path, falling back to legacy storage locations."""
|
||||||
|
path = self._get_session_path(key)
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
|
||||||
|
fallback_paths = [
|
||||||
|
(self._get_legacy_lossy_path(key), "legacy lossy path"),
|
||||||
|
(self._get_legacy_session_path(key), "legacy path"),
|
||||||
|
]
|
||||||
|
for fallback_path, description in fallback_paths:
|
||||||
|
if not fallback_path.exists():
|
||||||
|
continue
|
||||||
|
stored_key = self._stored_key_for_path(fallback_path)
|
||||||
|
if stored_key and stored_key != key:
|
||||||
|
logger.info(
|
||||||
|
"Skipping session {} from {} because it belongs to {}",
|
||||||
|
key,
|
||||||
|
description,
|
||||||
|
stored_key,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if not migrate:
|
||||||
|
return fallback_path
|
||||||
|
try:
|
||||||
|
shutil.move(str(fallback_path), str(path))
|
||||||
|
logger.info("Migrated session {} from {}", key, description)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to migrate session {}", key)
|
||||||
|
return None
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
def get_or_create(self, key: str) -> Session:
|
def get_or_create(self, key: str) -> Session:
|
||||||
"""
|
"""
|
||||||
Get an existing session or create a new one.
|
Get an existing session or create a new one.
|
||||||
@ -530,29 +563,8 @@ class SessionManager:
|
|||||||
|
|
||||||
def _load(self, key: str) -> Session | None:
|
def _load(self, key: str) -> Session | None:
|
||||||
"""Load a session from disk."""
|
"""Load a session from disk."""
|
||||||
path = self._get_session_path(key)
|
path = self._resolve_session_path(key, migrate=True)
|
||||||
if not path.exists():
|
if path is None:
|
||||||
fallback_paths = [
|
|
||||||
(self._get_legacy_lossy_path(key), "legacy lossy path"),
|
|
||||||
(self._get_legacy_session_path(key), "legacy path"),
|
|
||||||
]
|
|
||||||
for fallback_path, description in fallback_paths:
|
|
||||||
if not fallback_path.exists():
|
|
||||||
continue
|
|
||||||
stored_key = self._stored_key_for_path(fallback_path)
|
|
||||||
if stored_key and stored_key != key:
|
|
||||||
logger.info(
|
|
||||||
"Skipping migration for {} from {} because it belongs to {}",
|
|
||||||
key,
|
|
||||||
description,
|
|
||||||
stored_key,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
shutil.move(str(fallback_path), str(path))
|
|
||||||
logger.info("Migrated session {} from {}", key, description)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not path.exists():
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -831,8 +843,8 @@ class SessionManager:
|
|||||||
Returns ``{"key", "created_at", "updated_at", "metadata", "messages"}`` or
|
Returns ``{"key", "created_at", "updated_at", "metadata", "messages"}`` or
|
||||||
``None`` when the session file does not exist or fails to parse.
|
``None`` when the session file does not exist or fails to parse.
|
||||||
"""
|
"""
|
||||||
path = self._get_session_path(key)
|
path = self._resolve_session_path(key)
|
||||||
if not path.exists():
|
if path is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
messages: list[dict[str, Any]] = []
|
messages: list[dict[str, Any]] = []
|
||||||
@ -862,7 +874,7 @@ class SessionManager:
|
|||||||
}
|
}
|
||||||
except _SESSION_DATA_ERRORS as e:
|
except _SESSION_DATA_ERRORS as e:
|
||||||
logger.warning("Failed to read session {}: {}", key, e)
|
logger.warning("Failed to read session {}: {}", key, e)
|
||||||
repaired = self._repair(key)
|
repaired = self._repair(key, path=path)
|
||||||
if repaired is not None:
|
if repaired is not None:
|
||||||
logger.info("Recovered read-only session view {} from corrupt file", key)
|
logger.info("Recovered read-only session view {} from corrupt file", key)
|
||||||
return self._session_payload(repaired)
|
return self._session_payload(repaired)
|
||||||
@ -874,8 +886,8 @@ class SessionManager:
|
|||||||
This is used by WebUI routes that need session-level metadata but not the
|
This is used by WebUI routes that need session-level metadata but not the
|
||||||
full conversation transcript.
|
full conversation transcript.
|
||||||
"""
|
"""
|
||||||
path = self._get_session_path(key)
|
path = self._resolve_session_path(key)
|
||||||
if not path.exists():
|
if path is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
with open(path, encoding="utf-8") as f:
|
with open(path, encoding="utf-8") as f:
|
||||||
@ -898,7 +910,7 @@ class SessionManager:
|
|||||||
return None
|
return None
|
||||||
except _SESSION_DATA_ERRORS as e:
|
except _SESSION_DATA_ERRORS as e:
|
||||||
logger.warning("Failed to read session metadata {}: {}", key, e)
|
logger.warning("Failed to read session metadata {}: {}", key, e)
|
||||||
repaired = self._repair(key)
|
repaired = self._repair(key, path=path)
|
||||||
if repaired is not None:
|
if repaired is not None:
|
||||||
logger.info("Recovered read-only session metadata {} from corrupt file", key)
|
logger.info("Recovered read-only session metadata {} from corrupt file", key)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""Reproduction test: list_sessions drops corrupt legacy-stem sessions during repair."""
|
"""Regression tests for legacy-stem session handling."""
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -38,3 +38,39 @@ def test_list_sessions_repairs_corrupt_legacy_stem(tmp_path: Path, monkeypatch)
|
|||||||
# actual legacy filename. The session is silently dropped.
|
# actual legacy filename. The session is silently dropped.
|
||||||
assert len(sessions) == 1, f"Expected 1 session, got {len(sessions)}"
|
assert len(sessions) == 1, f"Expected 1 session, got {len(sessions)}"
|
||||||
assert sessions[0]["key"] == "telegram:12345"
|
assert sessions[0]["key"] == "telegram:12345"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_session_methods_fall_back_to_legacy_lossy_stem(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.session.manager.get_legacy_sessions_dir",
|
||||||
|
lambda: tmp_path / "legacy_sessions",
|
||||||
|
)
|
||||||
|
manager = SessionManager(tmp_path / "workspace")
|
||||||
|
session_id = "123e4567-e89b-12d3-a456-426614174000"
|
||||||
|
key = f"websocket:{session_id}"
|
||||||
|
legacy_path = manager._get_legacy_lossy_path(key)
|
||||||
|
assert legacy_path.name == f"websocket_{session_id}.jsonl"
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"_type": "metadata",
|
||||||
|
"key": key,
|
||||||
|
"created_at": datetime(2025, 1, 1).isoformat(),
|
||||||
|
"updated_at": datetime(2025, 1, 1).isoformat(),
|
||||||
|
"metadata": {
|
||||||
|
"workspace_scope": "project",
|
||||||
|
"project_path": "/tmp/example-project",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
legacy_path.write_text(json.dumps(metadata) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
metadata_result = manager.read_session_metadata(key)
|
||||||
|
file_result = manager.read_session_file(key)
|
||||||
|
|
||||||
|
assert metadata_result is not None
|
||||||
|
assert metadata_result["metadata"] == metadata["metadata"]
|
||||||
|
assert file_result is not None
|
||||||
|
assert file_result["metadata"] == metadata["metadata"]
|
||||||
|
assert file_result["messages"] == []
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user