mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
Refresh on upstream/main: resolve conflicts (no behavior change)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 1e70eaa49a)
This commit is contained in:
parent
0d5549a945
commit
4df16c429b
2 changed files with 93 additions and 0 deletions
|
|
@ -3835,6 +3835,23 @@ async def get_action_status(name: str, lines: int = 200):
|
|||
}
|
||||
|
||||
|
||||
# Per-row fields that no session LIST consumer reads but that dominate the
|
||||
# payload. ``system_prompt`` is the fully rendered prompt — tens of KB per
|
||||
# row — and made a 21-row /api/sessions response 528KB (96% dead weight),
|
||||
# re-fetched by the desktop sidebar on every refresh. The desktop's
|
||||
# SessionInfo type doesn't declare either field and the web UI never touches
|
||||
# them; ``GET /api/sessions/{id}`` detail reads stay complete. List callers
|
||||
# that genuinely need the full rows can pass ``?full=1``.
|
||||
_SESSION_LIST_HEAVY_FIELDS = ("system_prompt", "model_config")
|
||||
|
||||
|
||||
def _strip_session_list_rows(sessions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
for s in sessions:
|
||||
for key in _SESSION_LIST_HEAVY_FIELDS:
|
||||
s.pop(key, None)
|
||||
return sessions
|
||||
|
||||
|
||||
@app.get("/api/sessions")
|
||||
def get_sessions(
|
||||
limit: int = 20,
|
||||
|
|
@ -3845,6 +3862,7 @@ def get_sessions(
|
|||
source: str = None,
|
||||
exclude_sources: str = None,
|
||||
cwd_prefix: str = None,
|
||||
full: bool = False,
|
||||
profile: Optional[str] = None,
|
||||
):
|
||||
"""List sessions.
|
||||
|
|
@ -3858,6 +3876,9 @@ def get_sessions(
|
|||
start time) or ``recent`` (by latest activity across the compression
|
||||
chain). ``recent`` keeps a long-running conversation on the first page
|
||||
after it auto-compresses into a fresh continuation id.
|
||||
|
||||
Rows omit ``system_prompt``/``model_config`` (the payload-dominating
|
||||
fields no list UI reads) unless ``full=1`` is passed.
|
||||
"""
|
||||
if archived not in ("exclude", "only", "include"):
|
||||
raise HTTPException(
|
||||
|
|
@ -3914,6 +3935,8 @@ def get_sessions(
|
|||
s["is_default_profile"] = profile_name == "default"
|
||||
# SQLite stores the flag as 0/1; expose a real JSON boolean.
|
||||
s["archived"] = bool(s.get("archived"))
|
||||
if not full:
|
||||
_strip_session_list_rows(sessions)
|
||||
return {"sessions": sessions, "total": total, "limit": limit, "offset": offset}
|
||||
finally:
|
||||
db.close()
|
||||
|
|
@ -3934,6 +3957,7 @@ def get_profiles_sessions(
|
|||
profile: str = "all",
|
||||
source: str = None,
|
||||
exclude_sources: str = None,
|
||||
full: bool = False,
|
||||
):
|
||||
"""Unified, read-only session list aggregated across ALL profiles.
|
||||
|
||||
|
|
@ -3943,6 +3967,9 @@ def get_profiles_sessions(
|
|||
browsable list and only spins up a profile's backend when the user actually
|
||||
interacts (sends a message). A user with a single (default) profile gets the
|
||||
same rows as ``/api/sessions``, just tagged ``profile="default"``.
|
||||
|
||||
Rows omit ``system_prompt``/``model_config`` unless ``full=1`` — same
|
||||
list projection as ``/api/sessions``.
|
||||
"""
|
||||
if archived not in ("exclude", "only", "include"):
|
||||
raise HTTPException(status_code=400, detail="archived must be one of: exclude, only, include")
|
||||
|
|
@ -4033,6 +4060,8 @@ def get_profiles_sessions(
|
|||
sort_key = "last_active" if order == "recent" else "started_at"
|
||||
merged.sort(key=lambda s: s.get(sort_key) or s.get("started_at") or 0, reverse=True)
|
||||
window = merged[offset:offset + limit]
|
||||
if not full:
|
||||
_strip_session_list_rows(window)
|
||||
return {
|
||||
"sessions": window,
|
||||
"total": total,
|
||||
|
|
|
|||
|
|
@ -976,6 +976,70 @@ class TestWebServerEndpoints:
|
|||
assert captured["list"] == 3
|
||||
assert captured["count"] == 3
|
||||
|
||||
def _create_session_with_heavy_fields(self, session_id: str) -> None:
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB()
|
||||
try:
|
||||
db.create_session(
|
||||
session_id=session_id,
|
||||
source="cli",
|
||||
system_prompt="# SOUL.md\n" + ("prompt body " * 500),
|
||||
model_config={"temperature": 0.7, "notes": "x" * 200},
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def test_get_sessions_strips_heavy_fields_by_default(self):
|
||||
"""List rows must omit system_prompt/model_config.
|
||||
|
||||
system_prompt is the fully rendered prompt (tens of KB per row) and
|
||||
dominated the sidebar payload (96% of a 528KB response); no list UI
|
||||
reads it. Detail reads (GET /api/sessions/{id}) stay complete.
|
||||
"""
|
||||
self._create_session_with_heavy_fields("lean-list-row")
|
||||
|
||||
resp = self.client.get("/api/sessions?limit=20&offset=0")
|
||||
assert resp.status_code == 200
|
||||
rows = [s for s in resp.json()["sessions"] if s["id"] == "lean-list-row"]
|
||||
assert rows, "created session missing from list"
|
||||
row = rows[0]
|
||||
assert "system_prompt" not in row
|
||||
assert "model_config" not in row
|
||||
# The light fields the sidebar actually renders must survive.
|
||||
for key in ("id", "source", "started_at", "message_count", "is_active"):
|
||||
assert key in row
|
||||
|
||||
def test_get_sessions_full_param_keeps_heavy_fields(self):
|
||||
"""?full=1 is the escape hatch for callers that need complete rows."""
|
||||
self._create_session_with_heavy_fields("full-list-row")
|
||||
|
||||
resp = self.client.get("/api/sessions?limit=20&offset=0&full=1")
|
||||
assert resp.status_code == 200
|
||||
rows = [s for s in resp.json()["sessions"] if s["id"] == "full-list-row"]
|
||||
assert rows, "created session missing from list"
|
||||
row = rows[0]
|
||||
assert row["system_prompt"].startswith("# SOUL.md")
|
||||
assert "temperature" in (row["model_config"] or "")
|
||||
|
||||
def test_profiles_sessions_strips_heavy_fields_by_default(self):
|
||||
"""The cross-profile aggregate applies the same list projection."""
|
||||
self._create_session_with_heavy_fields("lean-profiles-row")
|
||||
|
||||
resp = self.client.get("/api/profiles/sessions?limit=20&offset=0")
|
||||
assert resp.status_code == 200
|
||||
rows = [s for s in resp.json()["sessions"] if s["id"] == "lean-profiles-row"]
|
||||
assert rows, "created session missing from profiles list"
|
||||
row = rows[0]
|
||||
assert "system_prompt" not in row
|
||||
assert "model_config" not in row
|
||||
assert row["profile"] == "default"
|
||||
|
||||
full = self.client.get("/api/profiles/sessions?limit=20&offset=0&full=1")
|
||||
assert full.status_code == 200
|
||||
full_rows = [s for s in full.json()["sessions"] if s["id"] == "lean-profiles-row"]
|
||||
assert full_rows and full_rows[0]["system_prompt"].startswith("# SOUL.md")
|
||||
|
||||
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)."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue