
    `gj                     x    d Z ddlmZ ddlZddlZddlmZmZmZm	Z	 ddgZ
 e	d      Zd
dZ G d	 dee         Zy)u  Shared concurrency helpers for plugin authors.

The most common plugin footgun is the lazy process-wide singleton:

    _client = None

    def get_client():
        global _client
        if _client is not None:
            return _client
        _client = ExpensiveClient(...)   # <-- TOCTOU: two threads both run this
        return _client

When two threads call ``get_client()`` before the singleton is set, both pass
the ``is not None`` guard, both run the expensive initialization, and the
second write clobbers the first — leaking whatever resource the first client
opened (connections, file handles, background threads).

Multi-threaded agent sessions share one process (delegated tool calls,
background workers, the self-improvement fork), so this race is reachable in
practice. Rather than make every plugin author remember to hand-roll
double-checked locking, this module gives them two thread-safe primitives:

* :func:`lazy_singleton` — decorator for the zero-arg accessor case.
* :class:`SingletonSlot` — manual slot for accessors that build different
  instances depending on a config/key argument.

Both are import-light (stdlib ``threading`` only) so any plugin can import
them without dragging in heavyweight host modules.
    )annotationsN)CallableGenericOptionalTypeVarlazy_singletonSingletonSlotTc                     t        j                         g t        j                         d fd       }dfd}||_        |S )a  Wrap a zero-argument factory into a thread-safe lazy singleton accessor.

    The wrapped callable returns the same instance on every call; the factory
    runs exactly once even under concurrent first calls, using double-checked
    locking. A ``.reset()`` attribute is attached for tests/teardown.

    Example::

        @lazy_singleton
        def get_client():
            return ExpensiveClient(load_config())

        client = get_client()   # built once, safe across threads
        get_client.reset()      # drop the instance (next call rebuilds)

    Note: if the factory raises, no instance is cached and the next call
    retries (the lock is released either way).
    c                     rd   S 5  rd   cd d d        S         } j                  |        | cd d d        S # 1 sw Y   y xY w)Nr   )append)instanceboxfactorylocks    G/root/.hermes/venv/lib/python3.12/site-packages/plugins/plugin_utils.pyaccessorz lazy_singleton.<locals>.accessorA   sS    q6M 	1v	 	 yHJJx 	 	 	s   ??Ac                 T    5   j                          d d d        y # 1 sw Y   y xY w)N)clear)r   r   s   r   resetzlazy_singleton.<locals>.resetL   s$     	IIK	 	 	s   ')returnr
   r   None)	threadingLock	functoolswrapsr   )r   r   r   r   r   s   `  @@r   r   r   +   sD    & >>DC__W  HNO    c                  4    e Zd ZdZdZddZd	dZd
dZddZy)r	   u)  Thread-safe lazy slot for accessors that take a build argument.

    Use this when the cached instance depends on a config/key passed to the
    accessor (so a bare zero-arg :func:`lazy_singleton` doesn't fit). The slot
    caches the first successfully-built instance and ignores the argument on
    subsequent calls — matching the established "first config wins" singleton
    semantics most plugins already rely on.

    Example::

        _slot: SingletonSlot[Honcho] = SingletonSlot()

        def get_honcho_client(config=None):
            return _slot.get(lambda: Honcho(**resolve(config)))

        def reset_honcho_client():
            _slot.reset()

    The factory runs at most once even under concurrent first calls. If the
    factory raises, nothing is cached and the next call retries.
    _lock_value_setc                R    t        j                         | _        d | _        d| _        y )NF)r   r   r!   r"   r#   selfs    r   __init__zSingletonSlot.__init__m   s    ^^%
#'	r   c                    | j                   r| j                  S | j                  5  | j                   r| j                  cd d d        S  |       }|| _        d| _         |cd d d        S # 1 sw Y   y xY w)NT)r#   r"   r!   )r&   r   values      r   getzSingletonSlot.getr   sc     99;;ZZ 	yy{{	 	 IEDKDI	 	 	s   A'A''A0c                6    | j                   r| j                  S dS )z?Return the cached instance without building it (None if unset).N)r#   r"   r%   s    r   peekzSingletonSlot.peek   s    "iit{{1T1r   c                b    | j                   5  d| _        d| _        ddd       y# 1 sw Y   yxY w)z;Drop the cached instance so the next ``get()`` rebuilds it.NFr    r%   s    r   r   zSingletonSlot.reset   s,    ZZ 	DKDI	 	 	s   %.Nr   )r   Callable[[], T]r   r
   )r   zOptional[T])	__name__
__module____qualname____doc__	__slots__r'   r*   r,   r    r   r   r	   r	   T   s"    , ,I
2r   )r   r.   r   r.   )r2   
__future__r   r   r   typingr   r   r   r   __all__r
   r   r	   r4   r   r   <module>r8      sD   > #   7 7_
-CL&R3GAJ 3r   