mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
When the window closed after the owner admitted a send but before the submit ack came back, its composer-outbox entry survived, so the reopened composer showed the words as a draft while the chat also showed them. The owner now stamps the admission's submission_id on the user row the turn persists (display_metadata.submission_id). Before re-offering an orphaned entry, the outbox asks whether the owner holds that exact submission: the session's pending snapshot (a queued admission has no row yet) and then the stored transcript. A send that landed is dropped silently; only a send the owner never received comes back as the draft. An unknown answer (no snapshot, failed read) still re-offers it, so the words are never lost.
82 lines
3.8 KiB
Python
82 lines
3.8 KiB
Python
"""Admission-scoped finite consumers, independent of viewer or daemon lifetime."""
|
|
from contextlib import contextmanager
|
|
from contextvars import ContextVar
|
|
|
|
from hermes_state_runtime import RuntimeStoreError
|
|
|
|
_finite_turn = ContextVar('finite_turn', default=None)
|
|
_unattended_turn = ContextVar('unattended_turn', default=False)
|
|
|
|
|
|
def finite_turn_required():
|
|
# None preserves legacy standalone CLI's marker. A bound owner turn must
|
|
# never inherit process launch flags from another viewer.
|
|
return _finite_turn.get()
|
|
|
|
|
|
FINITE_CLARIFY_NOTICE = (
|
|
"single-query mode: no user available to answer. Pick the best choices using your own "
|
|
"judgment, or make the most reasonable assumption you can, and continue.")
|
|
|
|
|
|
def finite_clarify_reply():
|
|
"""clarify_tool's reply on a finite turn (``chat -q`` / ``-z``): main's headless answer."""
|
|
return {"answers": {}, "outcome": "undelivered", "notice": FINITE_CLARIFY_NOTICE}
|
|
|
|
|
|
def unattended_turn():
|
|
# `hermes -z`: nobody can answer a prompt, so the classic one-shot contract auto-approves
|
|
# (legacy oneshot.py set HERMES_YOLO_MODE). `-q` stays single-query (approvals.single_query_mode).
|
|
return _unattended_turn.get()
|
|
|
|
|
|
@contextmanager
|
|
def finite_turn_scope(finite, unattended=False):
|
|
token = _finite_turn.set(finite)
|
|
unattended_token = _unattended_turn.set(bool(finite and unattended))
|
|
try:
|
|
yield
|
|
finally:
|
|
_unattended_turn.reset(unattended_token)
|
|
_finite_turn.reset(token)
|
|
|
|
|
|
def admit_finite(params, *, submitter=None):
|
|
"""Validate a submission's finite flags. *submitter* ``(connection, ref)`` marks a wire client: it may
|
|
ask for an unattended (auto-approving) turn only as the local CLI principal on its own CLI session."""
|
|
if 'unattended' in params and (type(params['unattended']) is not bool
|
|
or (params['unattended'] and params.get('finite') is not True)):
|
|
raise RuntimeStoreError('invalid_params')
|
|
if params.get('unattended') and submitter is not None and not _may_run_unattended(*submitter):
|
|
raise RuntimeStoreError('invalid_params')
|
|
if 'finite' not in params:
|
|
return {}
|
|
if type(params['finite']) is not bool:
|
|
raise RuntimeStoreError('invalid_params')
|
|
return {'finite': params['finite'], **({'unattended': True} if params.get('unattended') else {})}
|
|
|
|
|
|
def _may_run_unattended(connection, ref):
|
|
"""``hermes -z`` parity: main auto-approved only inside the one-shot's own process. Over the wire that
|
|
is the private local bootstrap ticket (no dashboard / remote identity), the session's creator (no
|
|
operator attach) and a CLI-surface session (never a Desktop, TUI, ACP or Bot Chat others watch)."""
|
|
from gateway.session_policy import policy_for_source
|
|
source = connection.authority.sessions[ref.session_id].source
|
|
if not connection.native_owner or source is None or source.user_id != connection.actor.subject:
|
|
return False
|
|
policy = policy_for_source(connection.authority.runner, source)
|
|
return policy is not None and policy.platform == 'cli'
|
|
|
|
|
|
async def execute_finite_admission(authority, ref, row):
|
|
from gateway.session_ingress import execute_admission
|
|
from gateway.session_display import display_turn_scope, submission_turn_scope
|
|
from gateway.session_surface import surface_turn_scope
|
|
from gateway.session_cli_turn import cli_turn_scope
|
|
with finite_turn_scope(row['payload'].get('finite', False), row['payload'].get('unattended') is True), \
|
|
surface_turn_scope(row['payload'].get('surface_v1')), \
|
|
display_turn_scope(row['admission_id'], row['payload'].get('display_v1')), \
|
|
submission_turn_scope(row['admission_id'], row.get('request_id')), \
|
|
cli_turn_scope(row['admission_id'], row['payload'].get('cli_turn_v1')):
|
|
return await execute_admission(authority, ref, row)
|