refactor(state): compact narrative docstrings and comments in hermes_state and hermes_state_sessions (keep every WHY)

This commit is contained in:
Teknium
2026-09-03 00:04:03 -07:00
parent cfb268a6c9
commit d944616a6d
2 changed files with 210 additions and 269 deletions
+120 -149
View File
@@ -25,8 +25,8 @@ logger = logging.getLogger("hermes_state")
def workspace_key(row: Dict[str, Any]) -> Optional[str]:
"""Workspace grouping key: git repo root, else cwd, else None (branch is
deliberately excluded so a checkout doesn't fragment history)."""
"""Workspace grouping key: git repo root, else cwd, else None (branch excluded: a checkout must not
fragment history)."""
return (row.get("git_repo_root") or "").strip() or (row.get("cwd") or "").strip() or None
@@ -61,8 +61,8 @@ def _cwd_prefix_clause(cwd_prefix: str) -> Tuple[str, List[str]]:
def _workspace_key_clause(key: str) -> Tuple[str, List[str]]:
"""WHERE for ``workspace_key(row) == key``: git_repo_root equals ``key``, or
(rows predating per-session git metadata) cwd is at/under ``key``."""
"""WHERE for ``workspace_key(row) == key``: git_repo_root equals ``key``, or (rows predating
per-session git metadata) cwd is at/under ``key``."""
prefix = key.rstrip("/\\") or key
cwd_clause, cwd_params = _cwd_prefix_clause(prefix)
return (
@@ -93,11 +93,9 @@ def _session_filter_where(
session_key: str = None, exclude_sources: List[str] = None, cwd_prefix: str = None,
min_message_count: int = 0, archived_only: bool = False, include_archived: bool = False,
) -> Tuple[List[str], List[Any]]:
"""Shared ``sessions s`` WHERE builder so counts line up with listed rows.
``exclude_children`` hides sub-agent runs and compression continuations but
keeps branch/reset children (``_LISTABLE_CHILD_SQL``: stable ``_branched_from``
marker OR the legacy heuristic for pre-marker rows). Clause order is part of
the SQL text contract."""
"""Shared ``sessions s`` WHERE builder so counts line up with listed rows. ``exclude_children``
hides sub-agent runs and compression continuations but keeps branch/reset children
(``_LISTABLE_CHILD_SQL``). Clause order is part of the SQL text contract."""
where: List[str] = []
params: List[Any] = []
if exclude_children:
@@ -121,8 +119,8 @@ def _session_filter_where(
def _collect_delegate_child_ids(conn, parent_ids: List[str]) -> List[str]:
"""Delegate-subagent ids (``_delegate_from`` marker, walked recursively) to
cascade-delete with *parent_ids*; untagged children stay orphaned, not deleted."""
"""Delegate-subagent ids (``_delegate_from`` marker, walked recursively) to cascade-delete with
*parent_ids*; untagged children stay orphaned, not deleted."""
df = _delegate_from_json()
seeds = {sid for sid in parent_ids if sid}
# Seed visited with the parents: a marker chain can loop back onto a parent,
@@ -163,9 +161,8 @@ _ERROR_FINISH_REASONS = frozenset({"error", "agent_error", "content_filter"})
def classify_session_status(role: Optional[str], has_tool_calls: bool, finish_reason: Optional[str]) -> str:
"""Error finish → ``error``; assistant with pending tool_calls or a trailing
user/tool row → ``interrupted``; otherwise ``complete`` (benign default:
pickers must not alarm on unknown shapes)."""
"""Error finish → ``error``; assistant with pending tool_calls or a trailing user/tool row →
``interrupted``; otherwise ``complete`` (benign default: pickers must not alarm on unknown shapes)."""
if (finish_reason or "").strip().lower() in _ERROR_FINISH_REASONS:
return SESSION_STATUS_ERROR
r = (role or "").strip().lower()
@@ -229,17 +226,17 @@ class SessionSessionsMixin:
"""Session rows: create/inherit, lifecycle flags, model_config, listing, deletion."""
def _own_profile_name(self) -> Optional[str]:
"""The profile owning THIS store, from ``db_path`` alone (``<root>/state.db``
→ default, ``<root>/profiles/<name>/state.db`` → name); path-based because a
gateway serving a NON-launch profile opens that profile's store. None
outside the profile tree — NULL beats a fabricated owner."""
"""The profile owning THIS store, from ``db_path`` alone (``<root>/state.db`` → default,
``<root>/profiles/<name>/state.db`` → name): a gateway serving a NON-launch profile opens that
profile's store. None outside the profile tree — NULL beats a fabricated owner."""
try:
from hermes_constants import get_default_hermes_root
root = get_default_hermes_root().resolve()
parent = Path(self.db_path).resolve().parent
if parent == root:
return "default"
if parent.parent == root / "profiles" and re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,63}", parent.name):
is_profile_dir = parent.parent == root / "profiles"
if is_profile_dir and re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,63}", parent.name):
return parent.name
except Exception:
logger.debug("own-profile derivation failed", exc_info=True)
@@ -247,12 +244,10 @@ class SessionSessionsMixin:
@staticmethod
def _inherit_parent_session_metadata(conn, session_id: str) -> None:
"""NULL-fill a child's cwd/git/profile from its parent (profile_name only
within the same ``agent:<ns>:`` namespace). The second UPDATE inherits
gateway routing columns ONLY for compression forks (a crash before the
gateway re-records the peer would strand the child unroutable); delegate
children must NOT inherit them (peer recovery could repoint gateway
traffic into a subagent's session)."""
"""NULL-fill a child's cwd/git/profile from its parent (profile_name only within the same
``agent:<ns>:`` namespace). Gateway routing columns are inherited ONLY by compression forks
(a crash before the gateway re-records the peer would strand the child unroutable); delegate
children must NOT inherit them (peer recovery could repoint traffic into a subagent's session)."""
conn.execute(_INHERIT_PARENT_META_SQL, (session_id,))
conn.execute(_INHERIT_PARENT_ROUTING_SQL, (session_id,))
@@ -263,11 +258,10 @@ class SessionSessionsMixin:
parent_session_id: str = None, cwd: str = None, profile_name: Optional[str] = None,
git_repo_root: str = None, origin_json: str = None, display_name: str = None,
) -> None:
"""Upsert a session row, COALESCE-filling NULL columns and never overwriting
what an earlier writer set (the gateway creates a bare row before
create_session carries the real model/prompt). chat_id/thread_id scope
gateway /resume (IDOR). Children backfill from the parent; a missing
profile_name is stamped with THIS store's own (NULL reads as unowned)."""
"""Upsert a session row, never overwriting what an earlier writer set (the gateway creates a
bare row before create_session carries the real model/prompt). chat_id/thread_id scope gateway
/resume (IDOR). Children backfill from the parent; a missing profile_name is stamped with THIS
store's own (NULL reads as unowned)."""
if not (profile_name or "").strip():
profile_name = self._own_profile_name()
def _do(conn):
@@ -348,9 +342,8 @@ class SessionSessionsMixin:
self, *, platform: str, chat_id: str, thread_id: Optional[str] = None,
user_id: Optional[str] = None,
) -> Optional[str]:
"""Most recent live session_id for source + chat_id (+ thread_id). With
``user_id`` exact sender matches win; several distinct users and no match
→ None rather than contaminating another participant's session."""
"""Most recent live session_id for source + chat_id (+ thread_id). With ``user_id`` exact sender
matches win; several distinct users and no match → None (never another participant's session)."""
if not platform or chat_id in (None, ""):
return None
query = """
@@ -377,14 +370,13 @@ class SessionSessionsMixin:
return None
return str(rows[0]["id"])
# Orphaned gateway-session repair: widest plausible gap between a keyed
# predecessor going quiet and its unkeyed successor (incident was ~60s; 15 min
# stays generous without spanning unrelated conversations).
# Orphaned gateway-session repair: widest plausible gap between a keyed predecessor going
# quiet and its unkeyed successor (incident was ~60s; 15 min without spanning conversations).
_ORPHAN_ADOPTION_MAX_GAP_S = 900.0
# Children that are NOT compression continuations (branches, delegates, tool
# sessions). Markers are bound to the queried parent id: continuations inherit
# model_config verbatim, so presence-matching misclassified them as delegates.
# Children that are NOT compression continuations (branches, delegates, tool sessions). Markers
# are bound to the queried parent id: continuations inherit model_config verbatim, so
# presence-matching misclassified them as delegates.
_NON_CONTINUATION_CHILD_FILTER_SQL = (
" AND COALESCE(json_extract(COALESCE({alias}model_config, '{{}}'),"
" '$._branched_from'), '') != ?\n"
@@ -393,9 +385,8 @@ class SessionSessionsMixin:
)
def end_session(self, session_id: str, end_reason: str) -> None:
"""Mark a session ended; the first end_reason wins (a compression split must
keep ``'compression'`` even if a stale end_session() targets it later).
reopen_session() first to deliberately re-end with a new reason."""
"""Mark a session ended; the first end_reason wins (a compression split must keep
``'compression'`` even if a stale end_session() lands later); reopen_session() to re-end."""
self._execute_write(lambda conn: self._end_and_bump(
conn, "UPDATE sessions SET ended_at = ?, end_reason = ? WHERE id = ? AND ended_at IS NULL",
(time.time(), end_reason, session_id), session_id, end_reason,
@@ -410,9 +401,9 @@ class SessionSessionsMixin:
return changed
def reopen_session(self, session_id: str) -> None:
"""Clear ended_at/end_reason so a session can be resumed; first stamp
markerless legacy reset children that depend on the parent's mutable
end_reason (WHERE shared with the listing predicate so they cannot drift)."""
"""Clear ended_at/end_reason so a session can be resumed; first stamp markerless legacy reset
children that depend on the parent's mutable end_reason (WHERE shared with the listing predicate
so they cannot drift)."""
def _do(conn):
conn.execute(
"UPDATE sessions AS child SET model_config = json_set("
@@ -428,10 +419,9 @@ class SessionSessionsMixin:
self._execute_write(_do)
def promote_to_session_reset(self, session_id: str, reason: str = "session_reset") -> bool:
"""Durably mark an intentional reset boundary on live rows or rows with a
*recoverable* accidental end_reason (explicit boundaries are preserved):
an ``agent_close`` row left recoverable would be resurrected by
stale-route recovery. Keep in sync with find_latest_gateway_session_for_peer."""
"""Durably mark an intentional reset boundary on live rows or rows with a *recoverable* accidental
end_reason (explicit boundaries are preserved): an ``agent_close`` row left recoverable would be
resurrected by stale-route recovery. Keep in sync with find_latest_gateway_session_for_peer."""
if not session_id:
return False
now = time.time()
@@ -450,11 +440,10 @@ class SessionSessionsMixin:
self, session_id: str, cwd: str, git_branch: Optional[str] = None,
git_repo_root: Optional[str] = None, replace_git_meta: bool = False,
) -> Optional[int]:
"""Persist the authoritative cwd and claim a Git metadata generation. git
fields are written only when non-empty (a probe failure never clobbers a
value) except under ``replace_git_meta`` (a workspace MOVE overwrites the
old repo identity). Async probes publish with the returned generation so an
older worker cannot overwrite a newer claim (A -> B -> A)."""
"""Persist the authoritative cwd and claim a Git metadata generation. git fields are written
only when non-empty (a probe failure never clobbers a value) except under ``replace_git_meta``
(a workspace MOVE overwrites the old repo identity). Async probes publish with the returned
generation so an older worker cannot overwrite a newer claim (A -> B -> A)."""
if not session_id or not cwd:
return None
branch = (git_branch or "").strip()
@@ -514,9 +503,8 @@ class SessionSessionsMixin:
self, session_id: str, ts: Optional[float] = None, *, description: Optional[str] = None,
provenance: Optional[ActivityProvenance] = None,
) -> None:
"""Stamp durable mid-turn activity (observation-only; rate-limited by the
caller) so surfaces see activity before any message row lands. Never moves
``last_activity_at`` backwards."""
"""Stamp durable mid-turn activity (observation-only; rate-limited by the caller) so surfaces see
activity before any message row lands. Never moves ``last_activity_at`` backwards."""
if not session_id:
return
when = float(ts if ts is not None else time.time())
@@ -532,8 +520,8 @@ class SessionSessionsMixin:
)
def clear_session_activity_labels(self, session_id: str) -> None:
"""Clear activity labels after a turn (``last_activity_at`` is kept so idle /
watchdog clocks stay continuous). A no-op clear skips the write transaction."""
"""Clear activity labels after a turn (``last_activity_at`` is kept so idle / watchdog clocks stay
continuous). A no-op clear skips the write transaction."""
if not session_id:
return
try:
@@ -571,18 +559,17 @@ class SessionSessionsMixin:
self._execute_write(_do)
def update_session_tool_names(self, session_id: str, tool_names: Optional[List[str]]) -> None:
"""Persist the resolved ``tools[]`` name order so a rebuilt AIAgent can't
fork the cached tool prefix on a flipped check_fn verdict; ``None`` clears."""
"""Persist the resolved ``tools[]`` name order so a rebuilt AIAgent can't fork the cached tool
prefix on a flipped check_fn verdict; ``None`` clears."""
payload = json.dumps(list(tool_names)) if tool_names is not None else None
self._write_sql("UPDATE sessions SET tool_names = ? WHERE id = ?", (payload, session_id))
def update_session_model(self, session_id: str, model: str, provider: Optional[str] = None) -> None:
"""Set the model after a mid-session /model switch (unconditionally), null
system_prompt so stale Model:/Provider: footers rebuild, and drop any
Browser runtime lock (lineage markers survive). *provider* is merged into
model_config so resume recombines the model with the provider that serves it."""
# Flush first: a still-queued pre-switch delta applied after this UPDATE
# would trip the first_accounted_route overwrite and resurrect the old route.
"""Set the model after a mid-session /model switch (unconditionally), null system_prompt so
stale Model:/Provider: footers rebuild, and drop any Browser runtime lock (lineage markers
survive). *provider* is merged into model_config so resume recombines model and provider."""
# Flush first: a still-queued pre-switch delta applied after this UPDATE would trip the
# first_accounted_route overwrite and resurrect the old route.
self.flush_token_counts()
patch: Dict[str, Any] = {"browser_model_lock": None}
if model:
@@ -600,9 +587,8 @@ class SessionSessionsMixin:
sql: str = "UPDATE sessions SET model_config = ? WHERE id = ?",
params: Optional[Callable[[Optional[str]], tuple]] = None,
) -> None:
"""Merge ``patch`` into model_config then run ``sql`` with ``params(merged)``
in one write transaction; no-op when the row doesn't exist. A custom ``sql``
(the prompt-nulling variants) also GCs unreferenced system_prompts."""
"""Merge ``patch`` into model_config then run ``sql`` with ``params(merged)`` in one write
transaction; no-op when the row doesn't exist. Custom ``sql`` (prompt-nulling) also GCs prompts."""
def _do(conn):
merged = self._merge_model_config_json(conn, session_id, patch)
if merged is _MODEL_CONFIG_ROW_MISSING:
@@ -615,10 +601,9 @@ class SessionSessionsMixin:
def _merge_model_config_json(
self, conn, session_id: str, patch: Dict[str, Any], *, on_missing: str = "skip",
):
"""SELECT + tolerant-parse + merge ``patch`` into model_config (the one place
that keeps ``_branched_from``/``_delegate_from`` alive); ``None`` deletes a
key. Returns serialized JSON (``None`` when empty, matching create_session's
NULL) or ``_MODEL_CONFIG_ROW_MISSING`` (``on_missing="raise"`` → ValueError)."""
"""SELECT + tolerant-parse + merge ``patch`` into model_config (the one place that keeps
``_branched_from``/``_delegate_from`` alive); ``None`` deletes a key. Returns serialized JSON
(``None`` when empty) or ``_MODEL_CONFIG_ROW_MISSING`` (``on_missing="raise"`` → ValueError)."""
row = conn.execute("SELECT model_config FROM sessions WHERE id = ?", (session_id,)).fetchone()
if row is None:
if on_missing == "raise":
@@ -649,8 +634,8 @@ class SessionSessionsMixin:
model_options: Optional[Dict[str, Any]] = None, route_source: Optional[str] = None,
confirmed: bool = False,
) -> None:
"""Persist a Browser / API-client runtime lock into model_config (lineage
markers survive); null system_prompt so cached footers cannot lie."""
"""Persist a Browser / API-client runtime lock into model_config (lineage markers survive); null
system_prompt so cached footers cannot lie."""
lock = {
"provider": provider or "", "model": model or "", "model_options": model_options or {},
"route_source": route_source or "", "confirmed": bool(confirmed), "updated_at": time.time(),
@@ -667,16 +652,14 @@ class SessionSessionsMixin:
)
def set_session_yolo(self, session_id: str, enabled: bool) -> None:
"""Persist the per-session YOLO flag so ``/yolo`` survives ``--resume``;
no-op when the row doesn't exist yet."""
"""Persist the per-session YOLO flag so ``/yolo`` survives ``--resume``; no-op without a row."""
if not session_id:
return
self._write_model_config_patch(session_id, {"yolo_mode": bool(enabled)})
@staticmethod
def session_yolo_enabled(session_meta: Optional[Dict[str, Any]]) -> bool:
"""Persisted YOLO flag; False on any parse failure (resume must never
enable the bypass by accident)."""
"""Persisted YOLO flag; False on any parse failure (resume must never enable the bypass)."""
return bool(_parse_model_config((session_meta or {}).get("model_config")).get("yolo_mode"))
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
@@ -690,8 +673,8 @@ class SessionSessionsMixin:
return self._session_row_dict(row) if row else None
def get_dominant_session_model_route(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Main-loop model route that served most API calls (``session_model_usage``
keeps the coherent per-call tuple; ``sessions`` mixes route changes)."""
"""Main-loop model route that served most API calls (``session_model_usage`` keeps the coherent
per-call tuple; ``sessions`` mixes route changes)."""
self.flush_token_counts()
row = self._read_one(
"""SELECT model, billing_provider, billing_base_url, billing_mode,
@@ -722,9 +705,8 @@ class SessionSessionsMixin:
return matches[0]["id"] if len(matches) == 1 else None
def backfill_null_session_profiles(self, profile_name: str) -> int:
"""Stamp this store's own profile onto legacy ``profile_name IS NULL`` rows,
which the fail-closed owner ladder cannot route (a store belongs to exactly
one profile). Never overwrites a non-NULL owner. Returns rows stamped."""
"""Stamp this store's own profile onto legacy ``profile_name IS NULL`` rows, which the fail-closed
owner ladder cannot route. Never overwrites a non-NULL owner. Returns rows stamped."""
stamp = (profile_name or "").strip()
if not stamp:
return 0
@@ -736,9 +718,8 @@ class SessionSessionsMixin:
) or 0)
def _set_lineage_column(self, column: str, session_id: str, value: Any) -> bool:
"""Set one ``sessions`` column across a whole compression lineage: Desktop
projects roots forward to their tip, so updating only the displayed tip
would let the untouched root resurrect it on refresh."""
"""Set one ``sessions`` column across a whole compression lineage: Desktop projects roots
forward to their tip, so updating only the tip would let the root resurrect it on refresh."""
return self._write_rowcount(
f"""
WITH RECURSIVE
@@ -781,8 +762,8 @@ class SessionSessionsMixin:
RECOVERABLE_END_REASONS = _RECOVERABLE_END_REASONS
def unarchive_recoverable_session(self, session_id: str) -> bool:
"""Un-archive a session archived by a recoverable accident (ws_orphan_reap,
agent_close); deliberate archives are left alone. True when un-archived."""
"""Un-archive a session archived by a recoverable accident (ws_orphan_reap, agent_close);
deliberate archives are left alone. True when un-archived."""
if not session_id:
return False
try:
@@ -811,19 +792,16 @@ class SessionSessionsMixin:
return True
def set_session_pinned(self, session_id: str, pinned: bool) -> bool:
"""Pin/unpin a session and its compression lineage (pins are exempt from the
``sessions.auto_archive`` sweep)."""
"""Pin/unpin a session and its compression lineage (pins are exempt from the auto_archive sweep)."""
return self._set_lineage_column("pinned", session_id, int(pinned))
def set_session_hidden(self, session_id: str, hidden: bool) -> bool:
"""Hide/unhide a session and its compression lineage from the default listing;
it stays resumable by the owning surface."""
"""Hide/unhide a session and its compression lineage from the default listing; still resumable."""
return self._set_lineage_column("hidden", session_id, int(hidden))
def set_session_read(self, session_id: str, read: bool = True) -> bool:
"""Mark read/unread across the compression lineage. ``last_read_at`` is a
watermark: unread when activity postdates it (no write on the message
path). NULL = never tracked = read; 0 = explicitly unread."""
"""Mark read/unread across the compression lineage. ``last_read_at`` is a watermark: unread when
activity postdates it (no write on the message path). NULL = never tracked = read; 0 = unread."""
return self._set_lineage_column("last_read_at", session_id, time.time() if read else 0.0)
@staticmethod
@@ -843,10 +821,9 @@ class SessionSessionsMixin:
@staticmethod
def _chain_search_where(where_sql: str, id_needle: str, search_needle: str) -> Tuple[str, List[Any]]:
"""Extend ``where_sql`` with the id_query / search_query filters: a row is
admitted when its own id or any id in its forward compression chain matches
(search also matches titles and a punctuation-stripped form so ``an94``
finds ``AN-94``); chain membership keeps the leading-wildcard LIKE bounded."""
"""Extend ``where_sql`` with the id_query / search_query filters: a row is admitted when its own
id or any id in its forward compression chain matches (search also matches titles and a
punctuation-stripped form so ``an94`` finds ``AN-94``); chain membership bounds the LIKE."""
params: List[Any] = []
clauses: List[str] = []
def like(needle: str) -> str:
@@ -878,9 +855,9 @@ class SessionSessionsMixin:
return (f"{where_sql} AND {combined}" if where_sql else f"WHERE {combined}"), params
def _project_compression_tips(self, sessions: List[Dict[str, Any]], compact_rows: bool) -> List[Dict[str, Any]]:
"""Replace each compression root's surfaced fields with its live tip's (root
``started_at`` kept for stable ordering), one batched query. ``_lineage_ids``
carries every id on the chain: a persisted tile can hold a MIDDLE segment's id."""
"""Replace each compression root's surfaced fields with its live tip's (root ``started_at`` kept
for stable ordering), one batched query. ``_lineage_ids`` carries every chain id (a tile may
hold a MIDDLE segment's id)."""
chain_by_root: Dict[str, List[str]] = {} # only roots whose tip differs from themselves
for s in sessions:
if s.get("end_reason") == "compression":
@@ -927,10 +904,9 @@ class SessionSessionsMixin:
id_query: str = None, search_query: str = None, compact_rows: bool = False,
include_pinned: bool = False, session_key: str = None, include_hidden: bool = False,
) -> List[Dict[str, Any]]:
"""List sessions with preview and ``last_active`` in one query.
``order_by_last_active`` sorts by the chain TIP via a recursive CTE (the only
path honouring ``id_query`` / ``search_query``); ``include_pinned`` back-fills
pins the page missed, still obeying the other filters."""
"""List sessions with preview and ``last_active`` in one query. ``order_by_last_active`` sorts
by the chain TIP via a recursive CTE (the only path honouring ``id_query`` / ``search_query``);
``include_pinned`` back-fills pins the page missed, still obeying the other filters."""
self.flush_token_counts() # rows carry token/cost totals
where_clauses, params = _session_filter_where(
exclude_children=not include_children, source=source, sources=sources, session_key=session_key,
@@ -947,7 +923,9 @@ class SessionSessionsMixin:
+ ("" if compact_rows else ", COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolved")
+ f",\n {_PREVIEW_COL_SQL},\n "
)
prompt_join = "" if compact_rows else "LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash"
prompt_join = (
"" if compact_rows else "LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash"
)
from_sessions = f"FROM sessions s\n {prompt_join}"
if order_by_last_active:
# The CTE walks compression-continuation edges forward from the admitted
@@ -1024,8 +1002,8 @@ class SessionSessionsMixin:
return sessions
def session_lifecycle_statuses(self, session_ids: List[str]) -> Dict[str, str]:
"""``{session_id: status}`` from each session's LAST message row (``'empty'``
when none); one query, MAX(id) per session joined back — never scans transcripts."""
"""``{session_id: status}`` from each session's LAST message row (``'empty'`` when none); one
query, MAX(id) per session joined back — never scans transcripts."""
ids = [sid for sid in (session_ids or []) if sid]
if not ids:
return {}
@@ -1050,9 +1028,9 @@ class SessionSessionsMixin:
return statuses
def assert_export_safe(self, session_id: str, max_messages: Optional[int] = None) -> int:
"""Active row count of this segment, or raise SessionExportTooLargeError (the
LIMITed subquery stops once the bound is exceeded). ``None`` resolves
``sessions.max_export_messages``; 0 disables the guard."""
"""Active row count of this segment, or raise SessionExportTooLargeError (the LIMITed subquery
stops once the bound is exceeded). ``None`` resolves ``sessions.max_export_messages``; 0 disables
the guard."""
from hermes_state import SessionExportTooLargeError, resolved_max_export_messages
if max_messages is None:
max_messages = resolved_max_export_messages()
@@ -1070,8 +1048,8 @@ class SessionSessionsMixin:
return message_count
def _is_explicit_branch_session(self, session_id: str) -> bool:
"""Copied user-facing branch (``_branched_from``)? Branches own a copied
transcript; compression continuations need the parent's archived rows."""
"""Copied user-facing branch (``_branched_from``)? Branches own a copied transcript;
compression continuations need the parent's archived rows."""
if not session_id:
return False
row = self._read_one("SELECT model_config FROM sessions WHERE id = ?", (session_id,))
@@ -1096,8 +1074,8 @@ class SessionSessionsMixin:
def search_sessions(
self, source: str = None, limit: int = 20, offset: int = 0, workspace_key: str = None,
) -> List[Dict[str, Any]]:
"""Sessions MRU-first with a computed ``last_active``; ``workspace_key`` scopes
to one workspace so ``hermes -c``/``--resume`` picks its last session."""
"""Sessions MRU-first with a computed ``last_active``; ``workspace_key`` scopes to one workspace
so ``hermes -c``/``--resume`` picks its last session."""
where_clauses = []
params: list = []
if source:
@@ -1121,8 +1099,7 @@ class SessionSessionsMixin:
min_message_count: int = 0, include_archived: bool = False, archived_only: bool = False,
exclude_children: bool = False, exclude_sources: List[str] = None,
) -> int:
"""Count sessions with list_sessions_rich's filters so a paired "load more"
total matches the listable rows."""
"""Count sessions with list_sessions_rich's filters so a paired "load more" total matches."""
where_clauses, params = _session_filter_where(
exclude_children=exclude_children, source=source, sources=sources,
exclude_sources=exclude_sources, cwd_prefix=cwd_prefix, min_message_count=min_message_count,
@@ -1131,8 +1108,7 @@ class SessionSessionsMixin:
return self._read_one(f"SELECT COUNT(*) FROM sessions s{_where_sql(where_clauses, ' ')}", params)[0]
def session_count_ge(self, n: int = 1) -> bool:
"""At least N sessions exist (archived included); LIMIT short-circuits
instead of session_count()'s index scan."""
"""At least N sessions exist (archived included); LIMIT short-circuits session_count()'s scan."""
return len(self._read_all("SELECT 1 FROM sessions LIMIT ?", (n,))) >= n
def session_count_by_source(
@@ -1155,8 +1131,8 @@ class SessionSessionsMixin:
return {str(row["source"]): int(row["count"] or 0) for row in rows}
def declared_scope_identity(self, session_id: str) -> Tuple[bool, str]:
"""(is_fork_child, source) in ONE read (prompt_cache_scope needs both from the
same row). Missing row → (False, ""); DB errors propagate (fail closed)."""
"""(is_fork_child, source) in ONE read (prompt_cache_scope needs both from the same row).
Missing row → (False, ""); DB errors propagate (fail closed)."""
session = self.get_session(session_id)
if not session:
return False, ""
@@ -1164,8 +1140,8 @@ class SessionSessionsMixin:
@staticmethod
def _remove_session_files(sessions_dir: Optional[Path], session_id: str) -> None:
"""Remove ``<id>.json``/``.jsonl`` and gateway ``request_dump_<id>_*.json``;
OSError is swallowed so a filesystem hiccup never blocks a DB operation."""
"""Remove ``<id>.json``/``.jsonl`` and gateway ``request_dump_<id>_*.json``; OSError is swallowed
so a filesystem hiccup never blocks a DB operation."""
if sessions_dir is None:
return
targets = [sessions_dir / f"{session_id}{suffix}" for suffix in (".json", ".jsonl")]
@@ -1180,8 +1156,8 @@ class SessionSessionsMixin:
pass
def get_session_delete_targets(self, session_id: str) -> List[str]:
"""Rows :meth:`delete_session` would remove: the session, then its recursive
delegate children (branch/compression children are orphaned, not deleted)."""
"""Rows :meth:`delete_session` would remove: the session, then its recursive delegate children
(branch/compression children are orphaned, not deleted)."""
with self._read_ctx() as conn:
if not conn.execute("SELECT 1 FROM sessions WHERE id = ? LIMIT 1", (session_id,)).fetchone():
return []
@@ -1192,10 +1168,9 @@ class SessionSessionsMixin:
self, session_id: str, sessions_dir: Optional[Path] = None,
expected_delete_ids: Optional[List[str]] = None,
) -> bool:
"""Delete a session and its messages; delegate children cascade,
branch/compression children are orphaned. *expected_delete_ids*: proceed
only if parent + delegate cascade still equals that set (re-walked inside
the transaction on purpose: export-before-delete fails closed)."""
"""Delete a session and its messages; delegate children cascade, branch/compression children
are orphaned. *expected_delete_ids*: proceed only if parent + delegate cascade still equals that
set (re-walked inside the transaction on purpose: export-before-delete fails closed)."""
removed_ids: List[str] = []
expected_ids = set(expected_delete_ids) if expected_delete_ids is not None else None
def _do(conn):
@@ -1220,8 +1195,8 @@ class SessionSessionsMixin:
return bool(deleted)
def delete_session_if_empty(self, session_id: str, sessions_dir: Optional[Path] = None) -> bool:
"""Delete *session_id* only if it has no messages, no title and no children;
check and delete share one transaction so a concurrent flush can't be lost."""
"""Delete *session_id* only if it has no messages, no title and no children; check and delete
share one transaction so a concurrent flush can't be lost."""
def _do(conn):
cursor = conn.execute(
"""
@@ -1247,9 +1222,8 @@ class SessionSessionsMixin:
return deleted
def delete_sessions(self, session_ids: List[str], sessions_dir: Optional[Path] = None) -> int:
"""Bulk delete with :meth:`delete_session` semantics per row, in ONE
transaction. Unknown ids are skipped (UI selection can race another tab's
delete). Returns the number that existed and were deleted."""
"""Bulk delete with :meth:`delete_session` semantics per row, in ONE transaction. Unknown ids
are skipped (UI selection can race another tab's delete). Returns the number deleted."""
unique_ids = list({sid for sid in session_ids or () if isinstance(sid, str) and sid})
if not unique_ids:
return 0
@@ -1276,22 +1250,20 @@ class SessionSessionsMixin:
self._remove_session_files(sessions_dir, sid)
return count
#: Shared by count_empty_sessions / delete_empty_sessions so badge and sweep
#: agree. ``message_count`` counts live rows only (rewind/compaction keep
#: dropped turns as ``active = 0``), so NOT EXISTS is the authority.
# Shared by count_empty_sessions / delete_empty_sessions so badge and sweep agree. message_count
# counts live rows only (rewind/compaction keep dropped turns as active = 0): NOT EXISTS is authority.
_EMPTY_SESSION_WHERE = (
"message_count = 0 AND ended_at IS NOT NULL AND archived = 0 AND NOT EXISTS ("
"SELECT 1 FROM messages WHERE messages.session_id = sessions.id)"
)
def count_empty_sessions(self) -> int:
"""Count of empty, ended, non-archived sessions; the ended_at guard means a
fresh session whose first message hasn't landed is never sniped."""
"""Count of empty, ended, non-archived sessions; ended_at guards a fresh session's first message."""
return self._read_one(f"SELECT COUNT(*) FROM sessions WHERE {self._EMPTY_SESSION_WHERE}")[0]
def delete_empty_sessions(self, sessions_dir: Optional[Path] = None) -> int:
"""Delete every empty, ended, non-archived session in one transaction,
orphaning (not cascading) children; transcript files are swept too."""
"""Delete every empty, ended, non-archived session in one transaction, orphaning (not cascading)
children; transcript files are swept too."""
removed_ids: list[str] = []
def _do(conn):
session_ids = {row["id"] for row in conn.execute(
@@ -1319,8 +1291,8 @@ class SessionSessionsMixin:
def archive_sessions(
self, older_than_days: Optional[float] = None, source: str = None, **filters,
) -> int:
"""Bulk soft-hide with prune_sessions' filter surface, via set_session_archived
so each lineage flips as a unit; idempotent. Returns matches."""
"""Bulk soft-hide with prune_sessions' filter surface, via set_session_archived so each lineage
flips as a unit; idempotent. Returns matches."""
filters.setdefault("archived", False)
rows = self.list_prune_candidates(older_than_days=older_than_days, source=source, **filters)
for row in rows:
@@ -1330,9 +1302,8 @@ class SessionSessionsMixin:
def maybe_auto_archive(
self, idle_days: float = 3, min_interval_hours: int = 24, exclude_pinned: bool = True,
) -> Dict[str, Any]:
"""Idempotent, non-destructive auto-archive of sessions idle for ``idle_days``;
``state_meta['last_auto_archive']`` gates runs within ``min_interval_hours``.
Never raises: {"skipped", "archived", "error"?}."""
"""Idempotent, non-destructive auto-archive of sessions idle for ``idle_days``; state_meta
``last_auto_archive`` gates runs within ``min_interval_hours``. Never raises."""
result: Dict[str, Any] = {"skipped": False, "archived": 0}
try:
now = time.time()