§
    øžyj†W  ã            #       óx  — U d Z ddlmZ ddlmZ ddlmZ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d+e	¬¦  «        Zee
d,<    ed-e	¬¦  «        Zee
d.<   i d
e“de“de“de“de“de“de“de“de“de“de“d!e“d#e“d%e“d)e“d+e“d-e“Z d/e!dd0fd1„Z"edOd/e!d0z  ded0         fd2„¦   «         Z#d3d3d3d3d3d3d3d3d3d3d3d3d3d4d3e	fd5e!d6e!d7e!d8e!d9e!d:e!d;e!d<e!d=e!d/e!d>e!d?e!d@e!dAedBe!dCede$f"dD„Z%dEe$dd0fdF„Z&dPdG„Z'dQdHe!dIe!de!fdJ„Z( e)h dK£¦  «        Z*defdL„Z+dPdM„Z,defdN„Z-d0S )Ra   
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", "")
é    )Úcontextmanager)Ú
ContextVar)ÚAnyÚIteratorÚ_UNSETFÚ_session_context_engagedÚreturnc                  ó   — t           S )zžTrue 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   © ó    ú=/home/ragecks/.hermes/hermes-agent/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_TYPEÚ_SESSION_CHAT_TYPEÚ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_CRON_SESSIONÚ_CRON_SESSIONÚ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Ú
session_idNc                 óž   — ddl }t                               | ¦  «         	 ddlm}  |¦   «         rdS n# t
          $ r Y nw xY w| |j        d<   dS )u]  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.

    Delegated subagent children are the exception: they are constructed inside
    the parent process within ``delegated_child_context()``, and their
    ``AIAgent.__init__`` calls this same helper. Writing a child's internal
    session id to ``os.environ`` (process-global) would clobber the parent's
    ``HERMES_SESSION_ID`` for the rest of the process â€” leaking the child id
    into parent tools and subprocesses spawned after the child was built. The
    ContextVar write below is task-local and safe for concurrent children; only
    the process-global ``os.environ`` mirror is suppressed for delegated
    children. Root agents (CLI, gateway, cron) keep both paths.
    r   N)Úis_delegated_child_contextr"   )Úosr#   ÚsetÚagent.delegation_contextr6   Ú	ExceptionÚenviron)r4   r7   r6   s      r   Úset_current_session_idr<   —   s†   € ð& €I€I€Iå‡O‚O�JÑÔÐðØGÐGÐGÐGÐGÐGà%Ð%Ñ'Ô'ð 	ØˆFð	øåð ð ð Øˆðøøøð '1€B„JÐ"Ñ#Ð#Ð#s    3 ³
A ¿A c              #   óð   K  — t                                ¦   «         }| �t                                | ¦  «         	 dV — t                                |¦  «         dS # t                                |¦  «         w xY w)a/  Bind a task-local session id and restore the prior value on exit.

    With ``session_id=None`` this acts as a save/restore boundary around code
    that may call :func:`set_current_session_id` itself (notably delegated
    ``AIAgent`` construction).  It intentionally never mutates ``os.environ``.
    N)r#   Úgetr8   )r4   Úpreviouss     r   Úscoped_current_session_idr@   ½   sm   è è € õ �ŠÑ Ô €HØÐÝ�Š˜
Ñ#Ô#Ð#ð"Øˆˆˆå�Š˜Ñ!Ô!Ð!Ð!Ð!ø��Š˜Ñ!Ô!Ð!Ð!øøøs   ¹A ÁA5Ú TÚplatformÚsourceÚchat_idÚ	chat_typeÚ	chat_nameÚ	thread_idÚuser_idÚ	user_nameÚsession_keyÚ
message_idÚprofileÚcwdÚasync_deliveryÚui_session_idÚcron_sessionc                 ó\  — da t                               | ¦  «        t                               |¦  «        t                               |¦  «        t
                               |¦  «        t                               |¦  «        t                               |¦  «        t                               |¦  «        t                               |¦  «        t                               |¦  «        t                               |	¦  «        t                               |¦  «        t                               |
¦  «        t                               |¦  «        t                               |¦  «        t                                t#          |¦  «        ¦  «        g}	 ddlm}  ||¦  «         n# t(          $ r Y nw xY w|S )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``.

    ``cron_session`` is tri-state: ``_UNSET`` preserves legacy
    ``os.environ["HERMES_CRON_SESSION"]`` fallback, ``"1"`` marks a cron job,
    and ``""`` explicitly marks a non-cron session while masking leaked env.
    Tr   )Úset_session_cwd)r   r   r8   r   r   r   r   r   r   r   r!   r#   r%   r'   r)   r+   r-   ÚboolÚagent.runtime_cwdrR   r:   )rB   rC   rD   rE   rF   rG   rH   rI   rJ   r4   rK   rL   rM   rN   rO   rP   ÚtokensrR   s                     r   Úset_session_varsrV   Î   sf  € ðR  $Ðå×Ò˜hÑ'Ô'Ý×Ò˜FÑ#Ô#Ý×Ò˜WÑ%Ô%Ý×Ò˜yÑ)Ô)Ý×Ò˜yÑ)Ô)Ý×Ò˜yÑ)Ô)Ý×Ò˜WÑ%Ô%Ý×Ò˜yÑ)Ô)Ý×Ò˜Ñ%Ô%Ý�Š˜
Ñ#Ô#Ý×"Ò" =Ñ1Ô1Ý×Ò 
Ñ+Ô+Ý×Ò˜WÑ%Ô%Ý×Ò˜,Ñ'Ô'Ý×#Ò#¥D¨Ñ$8Ô$8Ñ9Ô9ð€Fð"Ø5Ð5Ð5Ð5Ð5Ð5àˆ˜ÑÔÐÐøÝð ð ð Øˆðøøøà€Ms   Æ
F Æ
F)Æ(F)rU   c                 ód  — t           t          t          t          t          t
          t          t          t          t          t          t          t          t          fD ]}|                     d¦  «         Œt                               t           ¦  «         	 ddlm}  |¦   «          dS # t&          $ r Y dS 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).
    rA   r   ©Úclear_session_cwdN)r   r   r   r   r   r   r   r   r!   r#   r%   r'   r)   r+   r8   r-   r   rT   rY   r:   )rU   ÚvarrY   s      r   Úclear_session_varsr[     s¶   € õ 	ÝÝÝÝÝÝÝÝÝÝÝÝÝðð ð ˆð  	�Š�‰Œˆˆõ
 ×Ò¥Ñ'Ô'Ð'ðØ7Ð7Ð7Ð7Ð7Ð7àÐÑÔÐÐÐøÝð ð ð Øˆˆðøøøs   ÂB! Â!
B/Â.B/c                  óô   — t                                ¦   «         D ]} |                      t          ¦  «         Œt                               t          ¦  «         	 ddlm}  |¦   «          dS # t          $ r Y dS 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   rX   N)Ú_VAR_MAPÚvaluesr8   r   r-   rT   rY   r:   )rZ   rY   s     r   Úreset_session_varsr_   ;  s“   € õD �ŠÑ Ô ð ð ˆØ�Š•‰Œˆˆõ ×Ò¥Ñ'Ô'Ð'ðØ7Ð7Ð7Ð7Ð7Ð7àÐÑÔÐÐÐøÝð ð ð Øˆˆðøøøs   ÁA) Á)
A7Á6A7Únamer   c                 ó¬   — ddl }t                               | ¦  «        }|�|                     ¦   «         }|t          ur|S |                     | |¦  «        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)r7   r]   r>   r   Úgetenv)r`   r   r7   rZ   Úvalues        r   Úget_session_envrd   k  sT   € ð €I€I€Iå
�,Š,�tÑ
Ô
€CØ
€Ø—’‘	”	ˆØ�ÐÐØˆLà�9Š9�T˜7Ñ#Ô#Ð#r   >   rA   ÚcliÚtuiÚtoolÚcodexÚlocalÚkanbanÚdesktopÚgatewayÚwebhookÚ
api_serverÚmsgraph_webhookc                  ó  — ddl } |                      d¦  «        pt          dd¦  «        }t          dd¦  «        }||fD ]E}t          |pd¦  «                             ¦   «                              ¦   «         }|r|t          vr dS ŒFdS )	a7  Whether this turn is delivered over a human messaging channel.

    Callers use this to decide anything that differs between "the user is
    reading a chat message" and "the user is at a machine they own": whether
    to emit a delivery tag, whether a file has to land somewhere the gateway
    is allowed to send from, whether narration would read as chat noise.

    Resolves ``HERMES_PLATFORM``, then the session platform, then the session
    source, and reports messaging when any of them names a surface outside
    :data:`NON_MESSAGING_SESSION_SURFACES`.
    r   NÚHERMES_PLATFORMr   rA   r   TF)r7   rb   rd   ÚstrÚstripÚlowerÚNON_MESSAGING_SESSION_SURFACES)r7   rB   rC   Úidentitys       r   Úsession_is_messaging_surfacerw   ¢  s�   € ð €I€I€Ià�yŠyÐ*Ñ+Ô+Ð]­Ð?XÐZ\Ñ/]Ô/]€HÝÐ4°bÑ9Ô9€FØ˜vÐ&ð ð ˆÝ�x�~ 2Ñ&Ô&×,Ò,Ñ.Ô.×4Ò4Ñ6Ô6ˆØð 	˜Õ(FÐFÐFØ�4�4øØˆ5r   c                  ó:   — t                                d¦  «         dS )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-   r8   r   r   r   Údeclare_stateless_channelry   ¹  s   € õ, ×Ò Ñ&Ô&Ð&Ð&Ð&r   c                  ó¨   — ddl } | j                             d¦  «        rdS t                               ¦   «         }|t          u rdS t          |¦  «        S )uå  Whether the current session can deliver a background completion later.

    Returns ``False`` for finite runtimes that can end before a detached result
    is delivered: sessions explicitly 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`) â€” and dispatcher-spawned
    Kanban workers (identified by ``HERMES_KANBAN_TASK``), which are one-shot
    ``chat -q`` subprocesses. The real gateway platforms, the interactive CLI,
    and any other 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.
    r   NÚHERMES_KANBAN_TASKFT)r7   r;   r>   r-   r   rS   )r7   rc   s     r   Úasync_delivery_supportedr|   Ò  sV   € ð" €I€I€Ið 
„z‡~‚~Ð*Ñ+Ô+ð Øˆuå#×'Ò'Ñ)Ô)€EØ•€€ØˆtÝ�‰;Œ;Ðr   )N)r	   N)rA   ).Ú__doc__Ú
contextlibr   Úcontextvarsr   Útypingr   r   Úobjectr   Ú__annotations__r   rS   r   r   r   r   r   r   r   r   r   r!   r#   r%   r'   r)   r+   r-   r/   r1   r3   r]   rr   r<   r@   ÚlistrV   r[   r_   rd   Ú	frozensetru   rw   ry   r|   r   r   r   ú<module>r…      s  ðð$ð $ð $ðL &Ð %Ð %Ð %Ð %Ð %Ø "Ð "Ð "Ð "Ð "Ð "Ø  Ð  Ð  Ð  Ð  Ð  Ð  Ð  ð
 ˆf‰hŒh€ˆÐ Ð Ñ ð "'Ð ˜$Ð &Ð &Ñ &ð$ ð $ð $ð $ð $ð !+ 
Ð+DÈfÐ UÑ UÔ UÐ �:Ð UÐ UÑ UØ(˜jÐ)@È&ÐQÑQÔQ€�Ð QÐ QÑ QØ)˜zÐ*BÈFÐSÑSÔSÐ �*Ð SÐ SÑ SØ!+ Ð,FÐPVÐ!WÑ!WÔ!WÐ �JÐ WÐ WÑ WØ!+ Ð,FÐPVÐ!WÑ!WÔ!WÐ �JÐ WÐ WÑ WØ!+ Ð,FÐPVÐ!WÑ!WÔ!WÐ �JÐ WÐ WÑ WØ)˜zÐ*BÈFÐSÑSÔSÐ �*Ð SÐ SÑ SØ!+ Ð,FÐPVÐ!WÑ!WÔ!WÐ �JÐ WÐ WÑ WØ%˜:Ð&:ÀFÐKÑKÔK€ˆjÐ KÐ KÑ KØ$˜*Ð%8À&ÐIÑIÔI€ˆZÐ IÐ IÑ Ið &0 ZÐ0FÐPVÐ%WÑ%WÔ%WÐ ˜
Ð WÐ WÑ Wð #- *Ð-HÐRXÐ"YÑ"YÔ"YÐ �ZÐ YÐ YÑ Yà)˜zÐ*BÈFÐSÑSÔSÐ �*Ð SÐ SÑ Sð '˜JÐ'<ÀfÐMÑMÔM€ˆzÐ MÐ MÑ Mð( '1 jÐ1PÐZ`Ð&aÑ&aÔ&aÐ ˜Ð aÐ aÑ að +5¨*Ð5XÐbhÐ*iÑ*iÔ*iÐ ˜ZÐ iÐ iÑ iØ)3¨Ð4VÐ`fÐ)gÑ)gÔ)gÐ ˜JÐ gÐ gÑ gØ+5¨:Ð6ZÐdjÐ+kÑ+kÔ+kÐ ˜jÐ kÐ kÑ kðØÐ0ðà˜_ðð Ð.ðð Ð 2ð	ð
 Ð 2ðð Ð 2ðð Ð.ðð Ð 2ðð ˜,ðð ˜ðð Ð2ðð  Ð!4ðð Ð.ðð ˜=ðð (Ð)Dðð  'Ð(Bð!ð" )Ð*Fð#€ð*#1 sð #1¨tð #1ð #1ð #1ð #1ðL ð"ð "¨#°©*ð "ÀÈÄð "ð "ð "ñ „ð"ð" ØØØØØØØØØØØØØØØð!Að AØðAàðAð ðAð ð	Að
 ðAð ðAð ðAð ðAð ðAð ðAð ðAð ðAð 
ðAð ðAð ðAð  ð!Að" 
ð#Að Að Að AðH&˜tð &¨ð &ð &ð &ð &ðR-ð -ð -ð -ð`$ð $˜#ð $¨ð $°Sð $ð $ð $ð $ðJ "+ ðð ð ñ"ô "Ð ð$ dð ð ð ð ð.'ð 'ð 'ð 'ð2 $ð ð ð ð ð ð r   