mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Teknium's ruling: "I dont want to lock out desktop / mobile app users from a session. Please just
make it so that telegram doesn't get responses from A if they are on a /new'd convo".
When a messaging chat names another conversation (/new, /resume, /branch, a handoff, its next
inbound), every other live session on its route used to go read-only (session_superseded, cold
mounts since 7ba53f4756, handed-off chats since #144708, warm views in this PR's first commit).
Now they DETACH instead (gateway/session_detached.py): the live session moves off the chat's route
onto a key naming the conversation (agent:<ns>:detached:<sid>), pinned on a copy of its source
(SessionSource.detached_route, wire-invisible). Its turns then run on the ordinary messaging path:
- build_session_key answers the pinned key, so the running slot, agent cache, busy guard and the
agent a Stop reaches are the conversation's own, never the chat's;
- SessionStore.get_or_create_session resolves it to the conversation itself (its compression tip),
never creating or persisting a route;
- _delivery_adapter_for answers the profile's LocalSessionAdapter, the local sessions' delivery:
sends publish nothing, viewers read the canonical event stream (local_viewer_turn covers it, so
status, notices and approvals go there too). session_local_recovery.local_adapter creates it on
first use, since a messaging-only gateway has none until a local session restores.
The key has no platform slot, so a key parser (_parse_session_key) finds no chat to deliver to.
The chat naming the conversation again (/resume) moves it back onto its route (name_lane), and
its next turn delivers to the chat again. A cold open of a row the lane moved past mounts detached.
/new, /resume and /branch name the lane at once (_name_lane), not at the chat's next message, and
_publish_route does on every publish, so a warm view detaches the moment its chat moves on.
History mounts keep their own read-only gate (require_history_access). LiveSession.superseded,
history_mount and handed_off are gone (with route_recovery.mount/moved_local, which only fed them);
LiveSession.detached says what it means.
40 lines
1.9 KiB
Python
40 lines
1.9 KiB
Python
"""The picker's view of a canonical local session its last viewer closed, as main listed it.
|
|
|
|
Main's sidecar stamped ``tui_close`` (``/new``, a resume switch) and ``tui_shutdown`` (exit) on the
|
|
session row it owned; pickers then listed it as resumable. The stamp itself is the owner's
|
|
``session.detach {end_reason}`` (``session_acp_lifecycle.end_idle_viewer_session``); this module only
|
|
decides which live rows read as closed.
|
|
"""
|
|
from gateway.session_history_mount import is_history_mount
|
|
from hermes_state_local import local_receipt
|
|
from hermes_state_runtime import RuntimeStoreError
|
|
|
|
|
|
def _local_target(authority, session_id):
|
|
try:
|
|
return local_receipt(authority.db, session_id)['entry']['session_id']
|
|
except (RuntimeStoreError, KeyError, TypeError):
|
|
return None # not a local session (API binding, malformed receipt): nothing to read as closed
|
|
|
|
|
|
def closed_idle_unviewed(live, execution_state, target_ended_at):
|
|
"""The verdict on facts already read: the live listings read ``target_ended_at`` for a whole batch
|
|
in one off-loop query (``gateway/session_live_listing``)."""
|
|
if live is None or live.subscribers or execution_state != 'idle':
|
|
return False
|
|
if live.detached or is_history_mount(live):
|
|
return True # an old messaging conversation or a stored one: history once no one views it
|
|
return target_ended_at is not None
|
|
|
|
|
|
def closed_and_unviewed(authority, session_id, handle):
|
|
"""A live-feed row main would list as resumable: closed, idle and with no viewer left."""
|
|
live = authority.sessions.get(session_id)
|
|
if live is None or live.subscribers or handle.execution_state != 'idle':
|
|
return False
|
|
if live.detached or is_history_mount(live):
|
|
return True
|
|
target = _local_target(authority, session_id)
|
|
row = authority.db.get_session_list_row(target) if target else None
|
|
return closed_idle_unviewed(live, handle.execution_state, row and row.get('ended_at'))
|