fix(sessions): archive compressed conversation lineages
This commit is contained in:
parent
4829f8d2c5
commit
04b3f19538
@ -866,6 +866,12 @@ export function useSessionActions({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await setSessionArchived(storedSessionId, true, archived?.profile)
|
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 })
|
notify({ durationMs: 2_000, kind: 'success', message: copy.archived })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (archived) {
|
if (archived) {
|
||||||
|
|||||||
@ -1715,15 +1715,51 @@ class SessionDB:
|
|||||||
"""Archive or unarchive a session.
|
"""Archive or unarchive a session.
|
||||||
|
|
||||||
Archived sessions are hidden from the default session list but keep all
|
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
|
their messages — this is a soft hide, not a delete. For compression
|
||||||
row was updated.
|
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):
|
def _do(conn):
|
||||||
cursor = conn.execute(
|
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)
|
rowcount = self._execute_write(_do)
|
||||||
return rowcount > 0
|
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…
x
Reference in New Issue
Block a user