
    `gjB                        U d Z ddlmZ ddlmZ  e       Zeed<   dae	ed<   de	fdZ
 ed	e
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ede
      Zeed<    ed e
      Zeed!<    ed"e
      Zeed#<    ed$e
      Zeed%<    ed&e
      Zeed'<    ed(e
      Zeed)<   eeeeeeeeeeeeeeed*Zd+edd,fd-Z	 	 	 	 	 	 	 	 	 	 	 	 	 	 dDd.ed/ed0ed1ed2ed3ed4ed5ed+ed6ed7ed8ed9e	d:edefd;Zd<edd,fd=Z dEd>Z!dFd?ed@edefdAZ"dEdBZ#de	fdCZ$y,)Ga   
Session-scoped context variables for the Hermes gateway.

Replaces the previous ``os.environ``-based session state
(``HERMES_SESSION_PLATFORM``, ``HERMES_SESSION_CHAT_ID``, etc.) with
Python's ``contextvars.ContextVar``.

**Why this matters**

The gateway processes messages concurrently via ``asyncio``.  When two
messages arrive at the same time the old code did:

    os.environ["HERMES_SESSION_THREAD_ID"] = str(context.source.thread_id)

Because ``os.environ`` is *process-global*, Message A's value was
silently overwritten by Message B before Message A's agent finished
running.  Background-task notifications and tool calls therefore routed
to the wrong thread.

``contextvars.ContextVar`` values are *task-local*: each ``asyncio``
task (and any ``run_in_executor`` thread it spawns) gets its own copy,
so concurrent messages never interfere.

**Backward compatibility**

The public helper ``get_session_env(name, default="")`` mirrors the old
``os.getenv("HERMES_SESSION_*", ...)`` calls.  Existing tool code only
needs to replace the import + call site:

    # before
    import os
    platform = os.getenv("HERMES_SESSION_PLATFORM", "")

    # after
    from gateway.session_context import get_session_env
    platform = get_session_env("HERMES_SESSION_PLATFORM", "")
    )
ContextVar)Any_UNSETF_session_context_engagedreturnc                      t         S )zTrue if any session has been bound via set_session_vars in this process.

    See the ``_session_context_engaged`` comment for the leak-policy rationale.
    )r        J/root/.hermes/venv/lib/python3.12/site-packages/gateway/session_context.pysession_context_engagedr   >   s
    
 $#r
   HERMES_SESSION_PLATFORM)default_SESSION_PLATFORMHERMES_SESSION_SOURCE_SESSION_SOURCEHERMES_SESSION_CHAT_ID_SESSION_CHAT_IDHERMES_SESSION_CHAT_NAME_SESSION_CHAT_NAMEHERMES_SESSION_THREAD_ID_SESSION_THREAD_IDHERMES_SESSION_USER_ID_SESSION_USER_IDHERMES_SESSION_USER_NAME_SESSION_USER_NAMEHERMES_SESSION_KEY_SESSION_KEYHERMES_SESSION_ID_SESSION_IDHERMES_UI_SESSION_ID_SESSION_UI_SESSION_IDHERMES_SESSION_MESSAGE_ID_SESSION_MESSAGE_IDHERMES_SESSION_PROFILE_SESSION_PROFILEHERMES_SESSION_ASYNC_DELIVERY_SESSION_ASYNC_DELIVERY!HERMES_CRON_AUTO_DELIVER_PLATFORM_CRON_AUTO_DELIVER_PLATFORM HERMES_CRON_AUTO_DELIVER_CHAT_ID_CRON_AUTO_DELIVER_CHAT_ID"HERMES_CRON_AUTO_DELIVER_THREAD_ID_CRON_AUTO_DELIVER_THREAD_ID)r   r   r   r   r   r   r   r   r   r    r"   r$   r(   r*   r,   
session_idNc                 T    ddl }| |j                  d<   t        j                  |        y)a  Synchronize ``HERMES_SESSION_ID`` across ContextVar and ``os.environ``.

    Long-lived single-process entrypoints like the CLI can rotate sessions via
    ``/new``, ``/resume``, ``/branch``, or compression splits without
    reconstructing the entire agent. Tools still consult
    ``get_session_env("HERMES_SESSION_ID")`` with an ``os.environ`` fallback,
    so both storage paths must move together when the active session changes.
    r   Nr   )osenvironr   set)r.   r0   s     r   set_current_session_idr3      s"     &0BJJ"#OOJr
   platformsourcechat_id	chat_name	thread_iduser_id	user_namesession_key
message_idprofilecwdasync_deliveryui_session_idc                 f   da t        j                  |       t        j                  |      t        j                  |      t
        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |	      t        j                  |
      t        j                  t        |            g}	 ddlm}  ||       |S # t$        $ r Y |S w xY w)u  Set all session context variables and return reset tokens.

    Call ``clear_session_vars(tokens)`` in a ``finally`` block when the handler
    exits. Note ``clear_session_vars`` resets every var to ``""`` (to suppress
    the ``os.environ`` fallback) rather than restoring prior values — these
    helpers are not nestable/stack-safe, and the returned tokens are accepted
    only for API compatibility.

    ``cwd`` pins the logical working directory for this context.

    ``async_delivery`` declares whether this session's channel can route a
    background completion back to the agent after the turn ends (see
    ``_SESSION_ASYNC_DELIVERY`` / ``async_delivery_supported``). Stateless
    request/response adapters (the API server) pass ``False``.
    Tr   )set_session_cwd)r   r   r2   r   r   r   r   r   r   r   r   r!   r#   r%   r'   boolagent.runtime_cwdrB   	Exception)r4   r5   r6   r7   r8   r9   r:   r;   r.   r<   r=   r>   r?   r@   tokensrB   s                   r   set_session_varsrG      s    F  $h'F#W%y)y)W%y)%
#""=1
+W%##D$89F5 M  Ms   D# #	D0/D0rF   c                    t         t        t        t        t        t
        t        t        t        t        t        t        fD ]  }|j                  d        t        j                  t               	 ddlm}  |        y# t"        $ r Y yw xY w)a+  Mark session context variables as explicitly cleared.

    Sets all variables to ``""`` so that ``get_session_env`` returns an empty
    string instead of falling back to (potentially stale) ``os.environ``
    values.  The *tokens* argument is accepted for API compatibility with
    callers that saved the return value of ``set_session_vars``, but the
    actual clearing uses ``var.set("")`` rather than ``var.reset(token)``
    to ensure the "explicitly cleared" state is distinguishable from
    "never set" (which holds the ``_UNSET`` sentinel).
     r   clear_session_cwdN)r   r   r   r   r   r   r   r   r   r!   r#   r%   r2   r'   r   rD   rK   rE   )rF   varrK   s      r   clear_session_varsrM      sv     	  	& '7 s   /A= =	B	B	c                      t         j                         D ]  } | j                  t                t        j                  t               	 ddlm}  |        y# t        $ r Y yw xY w)uL  Reset every session context variable to ``_UNSET`` for THIS context.

    Distinct from :func:`clear_session_vars`, which sets the vars to ``""``
    ("explicitly cleared" — suppresses the os.environ fallback and is used when
    a handler *finishes*).  This helper restores the ``_UNSET`` sentinel
    ("never bound in this context"), which is what a freshly-spawned task should
    look like *before* it binds its own session.

    🔴 Why this exists — the cross-session ContextVar inheritance leak.
    Each gateway message is processed in its own ``asyncio`` task, created via
    ``create_task`` (which snapshots the *current* context with
    ``copy_context``).  When message B's task is spawned from a context where a
    concurrent message A had already called :func:`set_session_vars`, B inherits
    A's **set** ContextVars.  Until B calls its own ``set_session_vars`` there is
    a window where any subprocess B spawns (e.g. a tool shelling out) reads
    *A's* ``HERMES_SESSION_*`` identity via the subprocess-env bridge.  The
    bridge's ``_UNSET``-strip guard cannot help: the vars are not ``_UNSET``,
    they are set-to-A.  Calling ``reset_session_vars`` at the top of the
    per-message handler drops the inherited identity so the window strips safe
    (no session) instead of leaking the foreign one; the handler then binds its
    own via ``set_session_vars`` a few steps later.  See
    tests/tools/test_local_env_session_leak.py and
    tests/gateway/test_session_context_inheritance.py.

    Note ``_SESSION_ASYNC_DELIVERY`` lives outside ``_VAR_MAP`` (it is a bool
    capability flag read via :func:`async_delivery_supported`, not a string
    ``HERMES_SESSION_*`` env var read via :func:`get_session_env`), so it is
    reset explicitly below. Without it, a task spawned from a context where a
    sibling adapter bound ``async_delivery=False`` (the stateless API server)
    inherits that ``False`` through the pre-bind window, and
    ``async_delivery_supported`` wrongly reports the new turn's channel as
    unable to route a background completion until ``set_session_vars`` runs.
    r   rJ   N)_VAR_MAPvaluesr2   r   r'   rD   rK   rE   )rL   rK   s     r   reset_session_varsrQ      sV    D   
 '7 s   	A 	A#"A#namer   c                     ddl }t        j                  |       }||j                         }|t        ur|S |j	                  | |      S )u  Read a session context variable by its legacy ``HERMES_SESSION_*`` name.

    Drop-in replacement for ``os.getenv("HERMES_SESSION_*", default)``.

    Resolution order:
    1. Context variable (set by the gateway for concurrency-safe access).
       If the variable was explicitly set (even to ``""``) via
       ``set_session_vars`` or ``clear_session_vars``, that value is
       returned — **no fallback to os.environ**.
    2. ``os.environ`` (only when the context variable was never set in
       this context — i.e. CLI, cron scheduler, and test processes that
       don't use ``set_session_vars`` at all).
    3. *default*
    r   N)r0   rO   getr   getenv)rR   r   r0   rL   values        r   get_session_envrW   0  sC     
,,t
C
	L99T7##r
   c                  .    t         j                  d       y)aW  Declare that this session cannot receive an async background completion.

    Binds only the delivery capability, leaving every other session var unset.
    Use this instead of ``set_session_vars(async_delivery=False)`` on a pure
    single-process runner: ``set_session_vars`` also latches
    ``_session_context_engaged`` (see above), which switches the subprocess
    env bridge from "os.environ fallback" to "ContextVar-authoritative, strip on
    _UNSET" in ``tools/environments/local.py``. A one-shot CLI that never engages
    the session-context system must not flip that latch as a side effect of
    declaring a capability.

    Callers that already build a full session context (cron's ``run_job``) get
    the same state by passing ``async_delivery=False`` to ``set_session_vars``.

    A session that cannot take a late completion makes ``delegate_task`` fall
    through to its existing inline/synchronous path, so subagent results are
    returned within the turn instead of being dispatched to a channel that will
    never deliver them.

    See NousResearch/hermes-agent#53027 and #63142.
    FN)r'   r2   r	   r
   r   declare_stateless_channelrY   J  s    , &r
   c                  R    t         j                         } | t        u ryt        |       S )u  Whether the current session can deliver a background completion later.

    Returns ``False`` when the active session was bound by a stateless channel:
    an adapter that cannot route a notification back after the turn ends (the
    API server), or a one-shot runner that exits after its final response
    (``hermes -z``, cron — see :func:`declare_stateless_channel`). The real
    gateway platforms, the interactive CLI, and any path that never bound the
    contextvar return ``True``.

    Tools that promise async delivery (``terminal`` notify_on_complete /
    watch_patterns, ``delegate_task`` background=True) consult this before
    registering a watcher / dispatching a detached child, so they can refuse a
    promise the channel can't keep instead of silently no-op'ing.
    T)r'   rT   r   rC   )rV   s    r   async_delivery_supportedr[   c  s&     $'')E;r
   )rI   rI   rI   rI   rI   rI   rI   rI   rI   rI   rI   rI   TrI   )r   N)rI   )%__doc__contextvarsr   typingr   objectr   __annotations__r   rC   r   r   r   r   r   r   r   r   r   r   r!   r#   r%   r'   r)   r+   r-   rO   strr3   listrG   rM   rQ   rW   rY   r[   r	   r
   r   <module>rc      s  $L # 
 h  "' $ &$ $ !++Df U : U()@&Q Q)*BFS * S!+,FPV!W J W!+,FPV!W J W)*BFS * S!+,FPV!W J W%&:FKj K$%8&IZ I &00FPV%W 
 W #--HRX"Y Z Y)*BFS * S* '11PZ`&a  a +55Xbh*i Z i)34V`f)g J g+56Zdj+k j k  1,. 2 2. 2&$2!4.)D(B*F& s  t    999 9 	9
 9 9 9 9 9 9 9 
9 9 9 
9x$t $ $N-`$# $ $S $4'2$ r
   