mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 20:57:42 +02:00
[eric] dashboards: drop orphan session cards from GET so a dead-session card stops 404ing on load
This commit is contained in:
@@ -363,10 +363,45 @@ async def generate_name(dashboard_id: str):
|
||||
return {"name": dashboard.name, "auto_named": True}
|
||||
|
||||
|
||||
def _strip_orphan_session_cards(data: dict) -> None:
|
||||
"""Drop layout cards (and expanded ids) whose agent session no longer exists
|
||||
anywhere, in memory OR on disk. The frontend mounts an AgentChat per card and
|
||||
GETs its session; a card pointing at a vanished session (e.g. an empty
|
||||
never-saved session) 404s on every load and flashes a dead "connect a model"
|
||||
card before the client reconciles it away. The `gone()` test is the exact
|
||||
condition that makes GET /sessions/{id} 404, so it removes precisely those
|
||||
cards and nothing else. Filtering the RESPONSE (never the stored file) is
|
||||
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
|
||||
layout = data.get("layout")
|
||||
if not isinstance(layout, dict):
|
||||
return
|
||||
cards = layout.get("cards")
|
||||
if not isinstance(cards, dict):
|
||||
return
|
||||
|
||||
def gone(sid: str) -> bool:
|
||||
if sid.startswith("draft-") or sid in agent_manager.sessions:
|
||||
return False
|
||||
return _load_session_data(sid) is None
|
||||
|
||||
orphans = [sid for sid in cards if gone(sid)]
|
||||
for sid in orphans:
|
||||
cards.pop(sid, None)
|
||||
if orphans:
|
||||
exp = layout.get("expanded_session_ids")
|
||||
if isinstance(exp, list):
|
||||
layout["expanded_session_ids"] = [s for s in exp if s not in orphans]
|
||||
|
||||
|
||||
@dashboards.router.get("/{dashboard_id}")
|
||||
async def get_dashboard(dashboard_id: str):
|
||||
dashboard = _load(dashboard_id)
|
||||
return dashboard.model_dump(mode="json")
|
||||
data = dashboard.model_dump(mode="json")
|
||||
_strip_orphan_session_cards(data)
|
||||
return data
|
||||
|
||||
|
||||
@dashboards.router.put("/{dashboard_id}")
|
||||
|
||||
@@ -594,6 +594,33 @@ async def test_mcp_gate_only_forwards_activated_servers():
|
||||
assert forwarded == (set(active) & set(names)), f"mismatch for active={active}"
|
||||
|
||||
|
||||
def test_dashboard_get_strips_only_orphan_session_cards():
|
||||
"""A layout card whose session vanished (gone from memory AND disk) makes the
|
||||
frontend GET /sessions/{id} 404 on every load and flash a dead card. The
|
||||
dashboard GET filters those orphan cards out of the response, but must keep
|
||||
live (in-memory) cards, on-disk cards, and drafts. Non-destructive: only the
|
||||
response is filtered, never the stored layout."""
|
||||
from types import SimpleNamespace
|
||||
from backend.apps.dashboards import dashboards as D
|
||||
data = {"layout": {
|
||||
"cards": {
|
||||
"live": {"session_id": "live"}, # in memory
|
||||
"ondisk": {"session_id": "ondisk"}, # closed but on disk
|
||||
"draft-1": {"session_id": "draft-1"}, # unsent draft, no backend session yet
|
||||
"ghost": {"session_id": "ghost"}, # gone from memory AND disk -> would 404
|
||||
},
|
||||
"expanded_session_ids": ["live", "ghost"],
|
||||
}}
|
||||
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",
|
||||
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"
|
||||
assert data["layout"]["expanded_session_ids"] == ["live"], "ghost dropped from expanded too"
|
||||
|
||||
|
||||
def test_banned_models_not_offered():
|
||||
"""Claude Fable (banned) and Gemini 3.1 Pro (no working lane: AG can't serve
|
||||
it, AI Studio key 429s pro-preview) were pulled from the picker. Guard so a
|
||||
|
||||
Reference in New Issue
Block a user