diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index f576a1b4..5a43b91e 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -32,8 +32,8 @@ from backend.apps.agents.core.error_classify import ( # call time (dodging a circular import), and the disk-resilience test monkeypatches it here. from backend.config.paths import SESSIONS_DIR from backend.apps.agents.manager.session.session_store import ( - _save_session as save_session, - _load_session_data as load_session_data, + save_session, + load_session_data, ) from backend.apps.agents.manager.streaming.state import ThinkingState, TurnState from backend.apps.agents.manager.streaming.hook_context import HookContext diff --git a/backend/apps/agents/manager/AgentLaunchMixin.py b/backend/apps/agents/manager/AgentLaunchMixin.py index b73e3fd1..f69d0492 100644 --- a/backend/apps/agents/manager/AgentLaunchMixin.py +++ b/backend/apps/agents/manager/AgentLaunchMixin.py @@ -18,7 +18,7 @@ from backend.apps.agents.core.models import ( ) from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.settings.settings import load_settings -from backend.apps.agents.manager.session.session_store import _load_session_data as load_session_data +from backend.apps.agents.manager.session.session_store import load_session_data from backend.apps.agents.manager.session.apply_context_window import apply_context_window from backend.apps.agents.manager.session.workspace_git import ( detect_git_identity, diff --git a/backend/apps/agents/manager/MessagingMixin.py b/backend/apps/agents/manager/MessagingMixin.py index 69c4b4de..2482ccfe 100644 --- a/backend/apps/agents/manager/MessagingMixin.py +++ b/backend/apps/agents/manager/MessagingMixin.py @@ -15,8 +15,8 @@ from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.settings.settings import load_settings from backend.apps.agents.manager import browser_dispatch from backend.apps.agents.manager.session.session_store import ( - _load_session_data as load_session_data, - _save_session as save_session, + load_session_data, + save_session, ) from backend.apps.agents.manager.session.apply_context_window import apply_context_window from backend.apps.agents.manager.prompt.tool_catalog import get_all_tool_names diff --git a/backend/apps/agents/manager/browser_dispatch.py b/backend/apps/agents/manager/browser_dispatch.py index c5161340..8e6922d3 100644 --- a/backend/apps/agents/manager/browser_dispatch.py +++ b/backend/apps/agents/manager/browser_dispatch.py @@ -12,7 +12,7 @@ from typeguard import typechecked from backend.apps.agents.core.models import AgentSession, Message from backend.apps.agents.core.ws_manager import ws_manager -from backend.apps.agents.manager.session.session_store import _save_session as save_session +from backend.apps.agents.manager.session.session_store import save_session from backend.apps.settings.settings import load_settings logger = logging.getLogger(__name__) diff --git a/backend/apps/agents/manager/session/SessionLifecycleMixin.py b/backend/apps/agents/manager/session/SessionLifecycleMixin.py index 2a520d2c..79fd1338 100644 --- a/backend/apps/agents/manager/session/SessionLifecycleMixin.py +++ b/backend/apps/agents/manager/session/SessionLifecycleMixin.py @@ -12,9 +12,9 @@ from typeguard import typechecked from backend.apps.agents.core.models import AgentSession from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.agents.manager.session.session_store import ( - _delete_session_file as delete_session_file, - _load_all_session_data as load_all_session_data, - _save_session as save_session, + delete_session_file, + load_all_session_data, + save_session, build_search_text, ) from backend.apps.agents.manager.session.cloud_sync import sync_session_close diff --git a/backend/apps/agents/manager/session/lifecycle.py b/backend/apps/agents/manager/session/lifecycle.py index bb6447b6..ce00ab7e 100644 --- a/backend/apps/agents/manager/session/lifecycle.py +++ b/backend/apps/agents/manager/session/lifecycle.py @@ -11,7 +11,7 @@ from typeguard import typechecked from backend.apps.agents.core.models import AgentSession, Message, MessageBranch from backend.apps.agents.manager.session.apply_context_window import apply_context_window -from backend.apps.agents.manager.session.session_store import _load_session_data as load_session_data +from backend.apps.agents.manager.session.session_store import load_session_data @typechecked diff --git a/backend/apps/agents/manager/session/session_store.py b/backend/apps/agents/manager/session/session_store.py index 48b1069b..b1733db7 100644 --- a/backend/apps/agents/manager/session/session_store.py +++ b/backend/apps/agents/manager/session/session_store.py @@ -1,45 +1,53 @@ import os from backend.apps.agents.core.models import AgentSession +from typing import Dict, List, Optional, Tuple +from typeguard import typechecked from backend.config.json_store import read_json_or_none, atomic_write_json -def _sessions_dir() -> str: +@typechecked +def sessions_dir() -> str: # Resolve live so test patches on either the paths module or the # agent_manager facade re-export land on the same directory. from backend.apps.agents import agent_manager return agent_manager.SESSIONS_DIR -def _save_session(session_id: str, doc_data: dict): - sessions_dir = _sessions_dir() - os.makedirs(sessions_dir, exist_ok=True) - atomic_write_json(os.path.join(sessions_dir, f"{session_id}.json"), doc_data) +@typechecked +def save_session(session_id: str, doc_data: Dict) -> None: + dir_path = sessions_dir() + os.makedirs(dir_path, exist_ok=True) + atomic_write_json(os.path.join(dir_path, f"{session_id}.json"), doc_data) -def _load_session_data(session_id: str) -> dict | None: - return read_json_or_none(os.path.join(_sessions_dir(), f"{session_id}.json")) +@typechecked +def load_session_data(session_id: str) -> Optional[Dict]: + return read_json_or_none(os.path.join(sessions_dir(), f"{session_id}.json")) -def _delete_session_file(session_id: str): - path = os.path.join(_sessions_dir(), f"{session_id}.json") +@typechecked +def delete_session_file(session_id: str) -> None: + path = os.path.join(sessions_dir(), f"{session_id}.json") if os.path.exists(path): os.remove(path) -def _load_all_session_data() -> list[tuple[str, dict]]: +@typechecked +def load_all_session_data() -> List[Tuple[str, Dict]]: results = [] - sessions_dir = _sessions_dir() - if not os.path.exists(sessions_dir): + dir_path = sessions_dir() + if not os.path.exists(dir_path): return results - for fname in os.listdir(sessions_dir): + for fname in os.listdir(dir_path): if fname.endswith(".json"): - data = read_json_or_none(os.path.join(sessions_dir, fname)) + data = read_json_or_none(os.path.join(dir_path, fname)) if data is not None: results.append((fname[:-5], data)) return results +@typechecked def build_search_text(session: AgentSession, max_len: int = 5000) -> str: """Build a search-indexing string from the session name and message content.""" parts = [session.name or ""] diff --git a/backend/apps/auth/router.py b/backend/apps/auth/router.py index dbc892e7..49fd4f13 100644 --- a/backend/apps/auth/router.py +++ b/backend/apps/auth/router.py @@ -211,7 +211,7 @@ async def signout(): # Best-effort: failures here shouldn't block the sign-out itself. try: from backend.apps.agents.agent_manager import agent_manager - from backend.apps.agents.agent_manager import _save_session + from backend.apps.agents.manager.session.session_store import save_session running = list(agent_manager.tasks.keys()) for session_id in running: @@ -227,7 +227,7 @@ async def signout(): if sess.sdk_session_id: sess.sdk_session_id = None try: - _save_session(sess.id, sess.model_dump(mode="json")) + save_session(sess.id, sess.model_dump(mode="json")) except Exception as e: logger.warning("signout: save_session(%s) failed: %s", sess.id, e) diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py index f5fc346d..42faa2cb 100644 --- a/backend/apps/dashboards/dashboards.py +++ b/backend/apps/dashboards/dashboards.py @@ -374,7 +374,7 @@ def _strip_orphan_session_cards(data: dict) -> None: non-destructive: a wrong check can only hide a card for one response, not delete it. Drafts have no backend session yet, so they're always kept.""" from backend.apps.agents.agent_manager import agent_manager - from backend.apps.agents.manager.session.session_store import _load_session_data + from backend.apps.agents.manager.session.session_store import load_session_data layout = data.get("layout") if not isinstance(layout, dict): return @@ -385,7 +385,7 @@ def _strip_orphan_session_cards(data: dict) -> None: def gone(sid: str) -> bool: if sid.startswith("draft-") or sid in agent_manager.sessions: return False - return _load_session_data(sid) is None + return load_session_data(sid) is None orphans = [sid for sid in cards if gone(sid)] for sid in orphans: @@ -463,7 +463,7 @@ async def duplicate_dashboard(dashboard_id: str): now = datetime.now().isoformat() from backend.apps.agents.agent_manager import agent_manager - from backend.apps.agents.manager.session.session_store import _save_session + from backend.apps.agents.manager.session.session_store import save_session source_layout = source_data.get("layout", {}) or {} source_browser_cards = source_layout.get("browser_cards", {}) or {} @@ -513,7 +513,7 @@ async def duplicate_dashboard(dashboard_id: str): new_sess.browser_id = browser_id_remap[old_browser_id] if old_parent_sid and old_parent_sid in session_id_remap: new_sess.parent_session_id = session_id_remap[old_parent_sid] - _save_session(new_sess.id, new_sess.model_dump(mode="json")) + save_session(new_sess.id, new_sess.model_dump(mode="json")) new_cards: dict[str, dict] = {} for old_sid, card in source_cards.items(): diff --git a/backend/apps/swarm/entities/dashboards.py b/backend/apps/swarm/entities/dashboards.py index 7bf4aba4..65cf9b65 100644 --- a/backend/apps/swarm/entities/dashboards.py +++ b/backend/apps/swarm/entities/dashboards.py @@ -150,12 +150,12 @@ def _write(did: str, doc: dict) -> None: def _retag_sessions(session_ids, dashboard_id: str) -> None: # Best-effort: a hiccup here must not orphan the just-written dashboard. - from backend.apps.agents.manager.session.session_store import _load_session_data, _save_session + from backend.apps.agents.manager.session.session_store import load_session_data, save_session for sid in session_ids: try: - d = _load_session_data(sid) + d = load_session_data(sid) if d is not None: d["dashboard_id"] = dashboard_id - _save_session(sid, d) + save_session(sid, d) except Exception: pass diff --git a/backend/apps/swarm/entities/sessions.py b/backend/apps/swarm/entities/sessions.py index cea5dad2..4b441dc1 100644 --- a/backend/apps/swarm/entities/sessions.py +++ b/backend/apps/swarm/entities/sessions.py @@ -45,8 +45,8 @@ class SessionExportable: if sess is not None: d = sess.model_dump(mode="json") else: - from backend.apps.agents.manager.session.session_store import _load_session_data - d = _load_session_data(local_id) + from backend.apps.agents.manager.session.session_store import load_session_data + d = load_session_data(local_id) if d is None: return None return cls(local_id, d.get("name") or "Agent", d) @@ -85,7 +85,7 @@ class SessionExportable: @classmethod def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str: - from backend.apps.agents.manager.session.session_store import _save_session + from backend.apps.agents.manager.session.session_store import save_session sid = uuid4().hex now = datetime.now(timezone.utc).isoformat() # Older bundles (made before transcripts were carried) have no messages; @@ -118,10 +118,10 @@ class SessionExportable: "created_at": now, "closed_at": now, } - _save_session(sid, doc) + save_session(sid, doc) return sid @classmethod def rollback(cls, local_id: str) -> None: - from backend.apps.agents.manager.session.session_store import _delete_session_file - _delete_session_file(local_id) + from backend.apps.agents.manager.session.session_store import delete_session_file + delete_session_file(local_id) diff --git a/backend/tests/test_disk_resilience.py b/backend/tests/test_disk_resilience.py index 640297bf..323d3a39 100644 --- a/backend/tests/test_disk_resilience.py +++ b/backend/tests/test_disk_resilience.py @@ -77,11 +77,11 @@ def test_read_empty_file_returns_none(tmp_path): def test_sessions_skip_corrupt_and_roundtrip(tmp_path, monkeypatch): from backend.apps.agents import agent_manager as am - from backend.apps.agents.manager.session.session_store import _load_all_session_data + from backend.apps.agents.manager.session.session_store import load_all_session_data monkeypatch.setattr(am, "SESSIONS_DIR", str(tmp_path)) am.save_session("good", {"id": "good", "v": 1}) (tmp_path / "bad.json").write_text("{ truncated session ,,,") - loaded = dict(_load_all_session_data()) + loaded = dict(load_all_session_data()) assert loaded == {"good": {"id": "good", "v": 1}} assert (tmp_path / "bad.json").exists() # corrupt file preserved, not deleted assert am.load_session_data("good") == {"id": "good", "v": 1} diff --git a/backend/tests/test_swarm_bundle.py b/backend/tests/test_swarm_bundle.py index 9d6223be..55b37a36 100644 --- a/backend/tests/test_swarm_bundle.py +++ b/backend/tests/test_swarm_bundle.py @@ -204,7 +204,7 @@ def test_session_import_restores_transcript_without_granting_mcp(monkeypatch): from backend.apps.swarm.exportable import RemapTable from backend.apps.agents.manager.session import session_store saved: dict = {} - monkeypatch.setattr(session_store, "_save_session", lambda sid, doc: saved.update({sid: doc})) + monkeypatch.setattr(session_store, "save_session", lambda sid, doc: saved.update({sid: doc})) payload = { "name": "A", "model": "sonnet", "mode": "agent", "messages": [{"id": "m1", "role": "user", "content": "hi", "branch_id": "main"}], @@ -230,7 +230,7 @@ def test_session_import_old_bundle_without_transcript(monkeypatch): from backend.apps.swarm.exportable import RemapTable from backend.apps.agents.manager.session import session_store saved: dict = {} - monkeypatch.setattr(session_store, "_save_session", lambda sid, doc: saved.update({sid: doc})) + monkeypatch.setattr(session_store, "save_session", lambda sid, doc: saved.update({sid: doc})) sid = SessionExportable.import_({"name": "Old", "model": "sonnet"}, {}, RemapTable()) doc = saved[sid] assert doc["messages"] == [] diff --git a/backend/tests/test_v2_invariants.py b/backend/tests/test_v2_invariants.py index 063c94ea..9df8ba65 100644 --- a/backend/tests/test_v2_invariants.py +++ b/backend/tests/test_v2_invariants.py @@ -685,7 +685,7 @@ def test_dashboard_get_strips_only_orphan_session_cards(): fake_mgr = SimpleNamespace(sessions={"live": object()}) on_disk = {"ondisk": {"id": "ondisk"}} with patch("backend.apps.agents.agent_manager.agent_manager", fake_mgr), \ - patch("backend.apps.agents.manager.session.session_store._load_session_data", + patch("backend.apps.agents.manager.session.session_store.load_session_data", side_effect=lambda sid: on_disk.get(sid)): D._strip_orphan_session_cards(data) assert set(data["layout"]["cards"].keys()) == {"live", "ondisk", "draft-1"}, "only the ghost should be dropped"