Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3601,6 +3601,23 @@ async def get_action_status(name: str, lines: int = 200):
}


_SESSION_LIST_OMITTED_FIELDS = ("system_prompt", "model_config")


def _trim_session_list_payload(session: Dict[str, Any]) -> Dict[str, Any]:
"""Drop heavy/internal fields from session-list rows before JSON encoding.

``SessionDB.list_sessions_rich()`` returns the full ``sessions`` row for
CLI/internal consumers, including the cached system prompt. Desktop only
needs list metadata; serializing 40-50 full prompts turns every sidebar
refresh into multi-megabyte JSON and can starve the single uvicorn event
loop while the renderer retries timed-out requests.
"""
for key in _SESSION_LIST_OMITTED_FIELDS:
session.pop(key, None)
return session


@app.get("/api/sessions")
async def get_sessions(
limit: int = 20,
Expand Down Expand Up @@ -3671,6 +3688,7 @@ async def get_sessions(
)
now = time.time()
for s in sessions:
_trim_session_list_payload(s)
s["is_active"] = (
s.get("ended_at") is None
and (now - s.get("last_active", s.get("started_at", 0))) < 300
Expand Down Expand Up @@ -3783,6 +3801,7 @@ def get_profiles_sessions(
total += profile_total
profile_totals[name] = profile_total
for s in rows:
_trim_session_list_payload(s)
s["profile"] = name
s["is_default_profile"] = name == "default"
s["is_active"] = (
Expand Down
35 changes: 34 additions & 1 deletion tests/hermes_cli/test_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,32 @@ def close(self):
assert captured["list"] == 3
assert captured["count"] == 3

def test_get_sessions_omits_heavy_internal_prompt_fields(self):
"""The Desktop sidebar list must not serialize full cached prompts.

Real profiles can have 40-50 visible rows with ~40KB cached system
prompts each; returning those made every refresh multi-megabyte JSON.
"""
from hermes_state import SessionDB

db = SessionDB()
try:
db.create_session(
session_id="heavy-list-row",
source="cli",
system_prompt="prompt" * 10_000,
model_config={"large": "config" * 10_000},
)
db.append_message(session_id="heavy-list-row", role="user", content="hi")
finally:
db.close()

resp = self.client.get("/api/sessions?limit=20&min_messages=0")
assert resp.status_code == 200
row = next(s for s in resp.json()["sessions"] if s["id"] == "heavy-list-row")
assert "system_prompt" not in row
assert "model_config" not in row

def test_rename_session_updates_title(self):
"""PATCH /api/sessions/{id} renames a session (regression: the route
was missing entirely, so the desktop rename dialog got a 405)."""
Expand Down Expand Up @@ -768,7 +794,12 @@ def test_profiles_sessions_tags_default_profile(self):

db = SessionDB()
try:
db.create_session(session_id="agg-me", source="cli")
db.create_session(
session_id="agg-me",
source="cli",
system_prompt="prompt" * 10_000,
model_config={"large": "config" * 10_000},
)
db.append_message(session_id="agg-me", role="user", content="hi")
finally:
db.close()
Expand All @@ -779,6 +810,8 @@ def test_profiles_sessions_tags_default_profile(self):
row = next(s for s in data["sessions"] if s["id"] == "agg-me")
assert row["profile"] == "default"
assert row["is_default_profile"] is True
assert "system_prompt" not in row
assert "model_config" not in row
assert isinstance(data.get("errors"), list)

def test_profiles_sessions_rejects_unknown_archived_value(self):
Expand Down
Loading