fix(desktop): persist pins, reconnect after sleep, dedupe session search
Four related desktop session-management bugs: - Pins lost until refresh: pinned sessions are joined against the paginated in-memory session list, so a pinned chat that aged off the most-recent page got evicted on the next refresh (every message.complete triggers one) and the Pinned section went empty. mergeWorkingSessions -> mergeSessionPage now also preserves pinned rows (matched by live id or lineage root). Pin id checks in the chat header, command center, and delete/archive are normalized to the durable sessionPinId so pins survive auto-compression. - Stuck on "Starting Hermes" after sleep: macOS sleep drops the renderer WebSocket; nothing reconnected on wake so the composer stayed disabled. The gateway boot hook now auto-reconnects with backoff on close/error and on wake signals (powerMonitor resume/unlock-screen IPC, window online, visibilitychange). connect() gains an open timeout so a hung reconnect can't deadlock in 'connecting'. Composer placeholder distinguishes "Reconnecting to Hermes" from a cold start. - Loses chats from itself: the same hard-replace that dropped pins also dropped loaded sessions; mergeSessionPage keeps them. - Multiple copies/branches in search: /api/sessions/search deduped only by raw session_id, so compression segments and branches surfaced as separate hits. It now dedupes by lineage root and returns the live compression tip, matching the session_search tool's behavior.
This commit is contained in:
@@ -428,6 +428,44 @@ class TestWebServerEndpoints:
|
||||
tip = next(r for r in rows if r["id"] == "tip-new")
|
||||
assert tip.get("_lineage_root_id") == "root-old"
|
||||
|
||||
def test_search_dedupes_compression_lineage_to_tip(self):
|
||||
"""A conversation that auto-compresses leaves the matched term in both
|
||||
the root segment and the continuation. Search must collapse them to a
|
||||
single result keyed by the lineage root and pointing at the live tip,
|
||||
so the sidebar stops showing the same chat several times."""
|
||||
import time as _time
|
||||
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB()
|
||||
try:
|
||||
db.create_session(session_id="search-root", source="cli")
|
||||
db.append_message(session_id="search-root", role="user", content="distinctneedle in the root")
|
||||
db.end_session("search-root", "compression")
|
||||
now = _time.time()
|
||||
db._conn.execute(
|
||||
"UPDATE sessions SET started_at = ?, ended_at = ? WHERE id = ?",
|
||||
(now - 100, now - 90, "search-root"),
|
||||
)
|
||||
db.create_session(session_id="search-tip", source="cli", parent_session_id="search-root")
|
||||
db._conn.execute("UPDATE sessions SET started_at = ? WHERE id = ?", (now - 90, "search-tip"))
|
||||
db.append_message(session_id="search-tip", role="user", content="distinctneedle again in the tip")
|
||||
db._conn.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
resp = self.client.get("/api/sessions/search?q=distinctneedle")
|
||||
assert resp.status_code == 200
|
||||
results = resp.json()["results"]
|
||||
|
||||
lineage_hits = [r for r in results if r.get("lineage_root") == "search-root"]
|
||||
# One conversation -> exactly one result despite two FTS hits.
|
||||
assert len(lineage_hits) == 1
|
||||
hit = lineage_hits[0]
|
||||
# Surfaced under the live tip so clicking resumes the current session.
|
||||
assert hit["session_id"] == "search-tip"
|
||||
assert hit["lineage_root"] == "search-root"
|
||||
|
||||
def test_get_sessions_archived_is_boolean(self):
|
||||
from hermes_state import SessionDB
|
||||
|
||||
|
||||
Reference in New Issue
Block a user