fix(pheby): restore paged history across session resets
This commit is contained in:
@@ -235,22 +235,26 @@ def _display_user_text(text: str) -> str:
|
||||
return body.split(end, 1)[0] if end in body else text
|
||||
|
||||
|
||||
async def conversation_history(conversation_id: str, limit: int
|
||||
) -> Tuple[List[Dict[str, Any]], bool]:
|
||||
"""Load transcript rows for a conversation from Hermes state.db.
|
||||
async def conversation_history(conversation_id: str, limit: int,
|
||||
before_id: Optional[int] = None
|
||||
) -> Tuple[List[Dict[str, Any]], bool, bool]:
|
||||
"""Page the user-facing transcript across every reset generation.
|
||||
|
||||
Returns ``(messages, found)``. ``found`` is False when neither the
|
||||
session store nor the session DB knows the conversation.
|
||||
A gateway reset starts a new session ID for the same Pheby session key;
|
||||
compression can archive rows within any generation. Both are display
|
||||
history, even though neither belongs in the model's active context.
|
||||
Returns (messages, found, has_more) in ascending row-ID order.
|
||||
"""
|
||||
messages: List[Dict[str, Any]] = []
|
||||
found = False
|
||||
has_more = False
|
||||
|
||||
store = _session_store()
|
||||
session_id: Optional[str] = None
|
||||
session_key = _session_key_for(conversation_id)
|
||||
if store is not None:
|
||||
try:
|
||||
entry = await asyncio.to_thread(store.peek_session_id,
|
||||
_session_key_for(conversation_id))
|
||||
entry = await asyncio.to_thread(store.peek_session_id, session_key)
|
||||
if entry:
|
||||
session_id = str(entry)
|
||||
found = True
|
||||
@@ -258,43 +262,67 @@ async def conversation_history(conversation_id: str, limit: int
|
||||
logger.debug("[pheby] peek_session_id failed", exc_info=True)
|
||||
|
||||
db = _session_db()
|
||||
if db is not None and session_id:
|
||||
if db is not None:
|
||||
try:
|
||||
rows = await asyncio.to_thread(
|
||||
db.get_messages_as_conversation, session_id,
|
||||
include_row_ids=True)
|
||||
for row in rows[-limit:]:
|
||||
# The routing index only knows the current generation. Query the
|
||||
# persisted key to include resets even when parent_session_id is
|
||||
# absent. Hermes's own decoder handles archived/compacted rows.
|
||||
ids = await asyncio.to_thread(
|
||||
db._read_all,
|
||||
"SELECT id FROM sessions WHERE session_key = ? AND source = ? ORDER BY started_at, id",
|
||||
(session_key, "pheby"),
|
||||
)
|
||||
session_ids = [str(row["id"]) for row in ids]
|
||||
if session_id and session_id not in session_ids:
|
||||
session_ids.append(session_id)
|
||||
if session_ids:
|
||||
found = True
|
||||
rows = []
|
||||
for sid in session_ids:
|
||||
rows.extend(await asyncio.to_thread(
|
||||
db.get_messages_as_conversation, sid,
|
||||
include_row_ids=True, include_compacted=True))
|
||||
rows.sort(key=lambda row: int(row.get("_row_id") or 0))
|
||||
visible = []
|
||||
seen_replays = set()
|
||||
for row in rows:
|
||||
rid = row.get("_row_id")
|
||||
role = row.get("role")
|
||||
if role not in ("user", "assistant"):
|
||||
if not isinstance(rid, int) or role not in ("user", "assistant"):
|
||||
continue
|
||||
content = row.get("content")
|
||||
text = content if isinstance(content, str) else str(content or "")
|
||||
if role == "user":
|
||||
# Reset/compression can carry the same user turn into a
|
||||
# new generation; suppress exact replayed copies across
|
||||
# the entire transcript, before applying the page cursor.
|
||||
replay = (row.get("timestamp"), text)
|
||||
if replay in seen_replays:
|
||||
continue
|
||||
seen_replays.add(replay)
|
||||
text = _display_user_text(text)
|
||||
# Tool-call rows can surface as assistant rows with empty
|
||||
# content; skip empties so the client transcript stays clean.
|
||||
if before_id is not None and rid >= before_id:
|
||||
continue
|
||||
if not text.strip() and role == "assistant":
|
||||
continue
|
||||
messages.append({
|
||||
"message_id": f"m{row.get('_row_id')}"
|
||||
if isinstance(row.get("_row_id"), (int, str)) else None,
|
||||
"role": role,
|
||||
"text": text,
|
||||
visible.append({
|
||||
"message_id": f"m{rid}", "role": role, "text": text,
|
||||
"ts": _iso(row.get("timestamp")),
|
||||
})
|
||||
found = True
|
||||
except Exception:
|
||||
logger.debug("[pheby] transcript load failed", exc_info=True)
|
||||
has_more = len(visible) > limit
|
||||
messages = visible[-limit:]
|
||||
except Exception as exc:
|
||||
logger.warning("[pheby] transcript load failed", exc_info=True)
|
||||
raise RuntimeError("Unable to load conversation transcript") from exc
|
||||
|
||||
# A router-known conversation with no messages yet is still "found" so a
|
||||
# fresh client can open it as an empty chat.
|
||||
# A router-known conversation with no messages yet is still "found".
|
||||
if not found:
|
||||
server = _current_server()
|
||||
if server is not None:
|
||||
name = await server.router.get_name(conversation_id)
|
||||
if name is not None:
|
||||
found = True
|
||||
return messages, found
|
||||
return messages, found, has_more
|
||||
|
||||
|
||||
async def rename_conversation(conversation_id: str, name: str) -> bool:
|
||||
@@ -353,11 +381,20 @@ async def delete_conversation(conversation_id: str) -> bool:
|
||||
if not router_known and not session_id:
|
||||
return False
|
||||
|
||||
if session_id and db is not None:
|
||||
if db is not None:
|
||||
try:
|
||||
deleted = await asyncio.to_thread(db.delete_session, session_id)
|
||||
if deleted is False:
|
||||
return False
|
||||
rows = await asyncio.to_thread(
|
||||
db._read_all,
|
||||
"SELECT id FROM sessions WHERE session_key = ? AND source = ? ORDER BY started_at, id",
|
||||
(session_key, "pheby"),
|
||||
)
|
||||
ids = [str(row["id"]) for row in rows]
|
||||
if session_id and session_id not in ids:
|
||||
ids.append(str(session_id))
|
||||
if ids:
|
||||
deleted = await asyncio.to_thread(db.delete_sessions, ids)
|
||||
if deleted != len(ids):
|
||||
return False
|
||||
except Exception:
|
||||
logger.error("[pheby] session db delete failed", exc_info=True)
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user