mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
fix(sessions): let a compression continuation reclaim its base title
When context compression rotates a session, the original is ended and the continuation is auto-numbered (e.g. "name" -> "name #2"). The session list projects the ended root behind its live tip, so the user never sees the predecessor. But set_session_title's uniqueness check compared against ALL sessions, so renaming the visible tip back to "name" dead-ended with "Title 'name' is already in use by session <id the user can't find>". When the conflicting title is held by a compression ancestor of the session being renamed, transfer the title instead of raising: clear it from the ended predecessor and apply it to the continuation. Uniqueness is preserved (still exactly one session carries the title) and the parent-link lineage is untouched, so resume-by-title and tip projection keep working. Genuine conflicts with unrelated sessions, and with non-compression children (delegate/branch), still raise as before.
This commit is contained in:
parent
46f9d53468
commit
6ad0bc20f5
1 changed files with 65 additions and 3 deletions
|
|
@ -1836,6 +1836,48 @@ class SessionDB:
|
|||
|
||||
return cleaned
|
||||
|
||||
def _is_compression_ancestor(
|
||||
self, conn, *, ancestor_id: str, descendant_id: str
|
||||
) -> bool:
|
||||
"""Return True if *ancestor_id* is a compression predecessor of
|
||||
*descendant_id* (walking parent links up the continuation chain).
|
||||
|
||||
Uses the same edge definition as :meth:`get_compression_tip`: a
|
||||
parent → child edge counts as a compression continuation only when the
|
||||
parent ended with ``end_reason = 'compression'`` and the child started
|
||||
at or after the parent's ``ended_at`` (which distinguishes continuations
|
||||
from delegate subagents / branch children that also carry a
|
||||
``parent_session_id``).
|
||||
"""
|
||||
if not ancestor_id or not descendant_id or ancestor_id == descendant_id:
|
||||
return False
|
||||
current = descendant_id
|
||||
# Bound the walk defensively, mirroring get_compression_tip.
|
||||
for _ in range(100):
|
||||
row = conn.execute(
|
||||
"SELECT parent_session_id, started_at FROM sessions WHERE id = ?",
|
||||
(current,),
|
||||
).fetchone()
|
||||
if row is None or not row["parent_session_id"]:
|
||||
return False
|
||||
parent_id = row["parent_session_id"]
|
||||
parent = conn.execute(
|
||||
"SELECT ended_at, end_reason FROM sessions WHERE id = ?",
|
||||
(parent_id,),
|
||||
).fetchone()
|
||||
if (
|
||||
parent is None
|
||||
or parent["end_reason"] != "compression"
|
||||
or parent["ended_at"] is None
|
||||
or row["started_at"] is None
|
||||
or row["started_at"] < parent["ended_at"]
|
||||
):
|
||||
return False
|
||||
if parent_id == ancestor_id:
|
||||
return True
|
||||
current = parent_id
|
||||
return False
|
||||
|
||||
def set_session_title(self, session_id: str, title: str) -> bool:
|
||||
"""Set or update a session's title.
|
||||
|
||||
|
|
@ -1854,9 +1896,29 @@ class SessionDB:
|
|||
)
|
||||
conflict = cursor.fetchone()
|
||||
if conflict:
|
||||
raise ValueError(
|
||||
f"Title '{title}' is already in use by session {conflict['id']}"
|
||||
)
|
||||
conflict_id = conflict["id"]
|
||||
# A compression continuation is the live, projected-forward
|
||||
# head of its conversation; its compressed predecessors are
|
||||
# ended and hidden from the session list (list_sessions_rich
|
||||
# projects roots → tip). When the title that "conflicts" is
|
||||
# held by such a hidden ancestor, the user has no way to free
|
||||
# it — renaming the visible tip back to the base name would
|
||||
# dead-end with "already in use by <session they can't see>".
|
||||
# Treat this as a transfer: move the title off the ancestor
|
||||
# onto the continuation. Uniqueness is preserved (still only
|
||||
# one session carries the exact title) and the parent-link
|
||||
# lineage is untouched.
|
||||
if self._is_compression_ancestor(
|
||||
conn, ancestor_id=conflict_id, descendant_id=session_id
|
||||
):
|
||||
conn.execute(
|
||||
"UPDATE sessions SET title = NULL WHERE id = ?",
|
||||
(conflict_id,),
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Title '{title}' is already in use by session {conflict_id}"
|
||||
)
|
||||
cursor = conn.execute(
|
||||
"UPDATE sessions SET title = ? WHERE id = ?",
|
||||
(title, session_id),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue