mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-16 09:31:37 +00:00
fix(sessions): archive compressed conversation lineages
This commit is contained in:
parent
4829f8d2c5
commit
04b3f19538
3 changed files with 98 additions and 5 deletions
|
|
@ -866,6 +866,12 @@ export function useSessionActions({
|
|||
|
||||
try {
|
||||
await setSessionArchived(storedSessionId, true, archived?.profile)
|
||||
// A sidebar refresh can race the optimistic removal while the PATCH is
|
||||
// in flight and briefly reinsert the still-unarchived backend row. Win
|
||||
// that race after the mutation succeeds so right-click → Archive does
|
||||
// not appear to do nothing until the next full refresh.
|
||||
setSessions(prev => prev.filter(s => s.id !== storedSessionId))
|
||||
$pinnedSessionIds.set($pinnedSessionIds.get().filter(id => id !== storedSessionId && id !== archivedPinId))
|
||||
notify({ durationMs: 2_000, kind: 'success', message: copy.archived })
|
||||
} catch (err) {
|
||||
if (archived) {
|
||||
|
|
|
|||
|
|
@ -1715,15 +1715,51 @@ class SessionDB:
|
|||
"""Archive or unarchive a session.
|
||||
|
||||
Archived sessions are hidden from the default session list but keep all
|
||||
their messages — this is a soft hide, not a delete. Returns True when a
|
||||
row was updated.
|
||||
their messages — this is a soft hide, not a delete. For compression
|
||||
chains, archive the whole logical conversation. Desktop lists compression
|
||||
roots projected forward to their latest continuation; updating only the
|
||||
displayed tip lets the still-unarchived root resurrect it on refresh.
|
||||
Returns True when at least one row was updated.
|
||||
"""
|
||||
def _do(conn):
|
||||
cursor = conn.execute(
|
||||
"UPDATE sessions SET archived = ? WHERE id = ?",
|
||||
(1 if archived else 0, session_id),
|
||||
"""
|
||||
WITH RECURSIVE
|
||||
ancestors(id) AS (
|
||||
SELECT ?
|
||||
UNION
|
||||
SELECT parent.id
|
||||
FROM ancestors a
|
||||
JOIN sessions child ON child.id = a.id
|
||||
JOIN sessions parent ON parent.id = child.parent_session_id
|
||||
WHERE parent.end_reason = 'compression'
|
||||
AND child.started_at >= parent.ended_at
|
||||
),
|
||||
descendants(id) AS (
|
||||
SELECT ?
|
||||
UNION
|
||||
SELECT child.id
|
||||
FROM descendants d
|
||||
JOIN sessions parent ON parent.id = d.id
|
||||
JOIN sessions child ON child.parent_session_id = parent.id
|
||||
WHERE parent.end_reason = 'compression'
|
||||
AND child.started_at >= parent.ended_at
|
||||
),
|
||||
lineage(id) AS (
|
||||
SELECT id FROM ancestors
|
||||
UNION
|
||||
SELECT id FROM descendants
|
||||
)
|
||||
UPDATE sessions
|
||||
SET archived = ?
|
||||
WHERE id IN (SELECT id FROM lineage)
|
||||
""",
|
||||
(session_id, session_id, 1 if archived else 0),
|
||||
)
|
||||
return cursor.rowcount
|
||||
rowcount = cursor.rowcount
|
||||
if rowcount is None or rowcount < 0:
|
||||
rowcount = conn.execute("SELECT changes()").fetchone()[0]
|
||||
return rowcount
|
||||
rowcount = self._execute_write(_do)
|
||||
return rowcount > 0
|
||||
|
||||
|
|
|
|||
51
tests/hermes_state/test_session_archiving.py
Normal file
51
tests/hermes_state/test_session_archiving.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_state import SessionDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(tmp_path):
|
||||
database = SessionDB(tmp_path / "state.db")
|
||||
try:
|
||||
yield database
|
||||
finally:
|
||||
database.close()
|
||||
|
||||
|
||||
def _compression_pair(db: SessionDB):
|
||||
base = time.time() - 100
|
||||
db.create_session("root", source="cli")
|
||||
db.create_session("tip", source="cli", parent_session_id="root")
|
||||
db._conn.execute(
|
||||
"UPDATE sessions SET started_at = ?, ended_at = ?, end_reason = 'compression', message_count = 1 WHERE id = 'root'",
|
||||
(base, base + 10),
|
||||
)
|
||||
db._conn.execute(
|
||||
"UPDATE sessions SET started_at = ?, message_count = 1 WHERE id = 'tip'",
|
||||
(base + 20,),
|
||||
)
|
||||
db._conn.commit()
|
||||
|
||||
|
||||
def test_archiving_compression_tip_archives_projected_root(db):
|
||||
_compression_pair(db)
|
||||
|
||||
assert db.set_session_archived("tip", True) is True
|
||||
|
||||
assert db.get_session("root")["archived"] == 1
|
||||
assert db.get_session("tip")["archived"] == 1
|
||||
assert [s["id"] for s in db.list_sessions_rich(order_by_last_active=True)] == []
|
||||
assert [s["id"] for s in db.list_sessions_rich(order_by_last_active=True, archived_only=True)] == ["tip"]
|
||||
|
||||
|
||||
def test_unarchiving_compression_tip_unarchives_projected_root(db):
|
||||
_compression_pair(db)
|
||||
db.set_session_archived("tip", True)
|
||||
|
||||
assert db.set_session_archived("tip", False) is True
|
||||
|
||||
assert db.get_session("root")["archived"] == 0
|
||||
assert db.get_session("tip")["archived"] == 0
|
||||
assert [s["id"] for s in db.list_sessions_rich(order_by_last_active=True)] == ["tip"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue