
    oq'j                         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dS )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TfactoryCallable[[], T]returnc                     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).
    r   r
   c                     rd         S 5  rd         cd d d            S              }                      |            | cd d d            S # 1 swxY w Y   d S )Nr   )append)instanceboxr   locks    9/home/ubuntu/.hermes/hermes-agent/plugins/plugin_utils.pyaccessorz lazy_singleton.<locals>.accessorA   s     	q6M 	 	 1v	 	 	 	 	 	 	 	 wyyHJJx   	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   
A AAANonec                 f    5                                     d d d            d S # 1 swxY w Y   d S )N)clear)r   r   s   r   resetzlazy_singleton.<locals>.resetL   sz     	 	IIKKK	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   &**)r   r
   r   r   )	threadingLock	functoolswrapsr   )r   r   r   r   r   s   `  @@r   r   r   +   s    & >DC_W              HNO    c                  6    e Zd ZdZdZddZdd	ZddZddZdS )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_setr   r   c                R    t          j                    | _        d | _        d| _        d S )NF)r   r   r"   r#   r$   selfs    r   __init__zSingletonSlot.__init__m   s#    ^%%
#'			r   r   r   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xY w Y   d S )NT)r$   r#   r"   )r'   r   values      r   getzSingletonSlot.getr   s     9 	;Z 	 	y #{	 	 	 	 	 	 	 	 GIIEDKDI	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   AAAAOptional[T]c                "    | j         r| j        ndS )z?Return the cached instance without building it (None if unset).N)r$   r#   r&   s    r   peekzSingletonSlot.peek   s    "i1t{{T1r   c                b    | j         5  d| _        d| _        ddd           dS # 1 swxY w Y   dS )z;Drop the cached instance so the next ``get()`` rebuilds it.NFr!   r&   s    r   r   zSingletonSlot.reset   s|    Z 	 	DKDI	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   $((Nr   )r   r   r   r
   )r   r,   )	__name__
__module____qualname____doc__	__slots__r(   r+   r.   r    r   r   r	   r	   T   su         , ,I   
   2 2 2 2     r   )r   r   r   r   )r3   
__future__r   r   r   typingr   r   r   r   __all__r
   r   r	   r5   r   r   <module>r9      s    > # " " " " "         7 7 7 7 7 7 7 7 7 7 7 7_
-GCLL& & & &R3 3 3 3 3GAJ 3 3 3 3 3r   