
    )j0                       U d Z ddlZddlZddlZddlZddlZddlZddlZddlZ	ddl
Z
ddlZddlZddlZddlmZmZ ddlmZ ddlmZ  ej        e          Z eh d          Z eddh          Z ed	d
h          ZdZdefdZdededefdZ ddedz  de!dz  fdZ"dedz  fdZ#ddede$de$fdZ%dede&fdZ'dede&defdZ(dede&de&fdZ)dede$fdZ*dedz  fd Z+d!ede,ee&dz  f         fd"Z-de.e         fd#Z/dd$eded%e&dz  de$fd&Z0d'ee.e         z  e,ed(f         z  e1e         z  dz  de$fd)Z2	 ddd*d+edz  d'ee.e         z  e,ed(f         z  e1e         z  dz  dedz  fd,Z3d-edz  de!fd.Z4d-edz  de,e!e!f         fd/Z5dd0ed1edz  de$fd2Z6ddl7Z7dd3l7m8Z8m9Z9 dd4l:m:Z: dd5l;m<Z< dd6l=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZDmEZE dd7lFmGZG dd5l;m<ZH ejI        J                    d e eHeK          L                                jM        d8                              dd9lNmOZOmPZP dd:lQmRZRmSZS dd;lTmUZUmVZVmWZW d<ZXdd>ed?e&defd@ZYdA ZZ eVdBdC          Z[de<fdDZ\dEe]de$fdFZ^ddEe]dedefdHZ_dd>ededIe&defdJZ`ddLe&de&fdMZa eVdNdO          Zbde<fdPZcddEe]dedefdQZddd>ededIe&defdRZe eVdSdT          ZfdUdVdWdXdYdZZgde<fd[ZhddEe]dedefd]Zi eVd^d_          Zj eVd`da          Zk eW            Zl eU            ZmdbZndcZoddZpdeZqe[ebefejekeldCz  eldOz  eldTz  eld_z  eldaz  eldfz  dgz  eldfz  dhz  eldfz  diz  eldfz  djz  eldfz  dkz  fZrdlZsdmZtdnZude?e<         fdoZvdefdpZwde$fdqZxde?e<         fdrZydse<de$fdtZzdse<duede$fdvZ{dwe<dxe<de$fdyZ|dwede@e         fdzZ} ej~        d{          Zdwedefd|Zi d}d~dddddddddddddddddddddddddddddddddddZddddddZdZeDed(f         ed<   d                     ed eD             ed                    Z ej~        dez   dz   ej                  Zde<fdZdEe]dedefdZddLe&de&fdZe8 G d d                      ZdededefdZdddddEe]dedede@e         de@e         f
dZ G d deG          Z G d deG          Ze8 G d d                      Ze8 G d d                      Z ej~        dej                   ej~        dej                   ej~        dej                  fZe,ej        e         d(f         ed<   ddZe8 G d dæ                      Z G dĄ de          ZddƜde>eef         dedede$ddf
dʄZdZeBegeCe@eEedf                           f         Z	 dde!dededz  dedz  fdτZ	 dde!dededz  de.e         dz  fdЄZdedefd҄Z G dӄ de          ZdS )z
Base platform adapter interface.

All platform adapters (Telegram, Discord, WhatsApp, Weixin, and more) inherit from this
and implement the required methods.
    N)ABCabstractmethod)urlsplit)normalize_proxy_url   .m4a.mp3.ogg.wav.flac.opusr	   r   r
   r         >@returnc                 j    t          | d|           }t          |pd                                          S )z=Normalize a Platform enum / raw string into a lowercase name.value )getattrstrlower)platformr   s     ;/home/ubuntu/.hermes/hermes-agent/gateway/platforms/base.py_platform_namer   '   s1    Hgx00Eu{!!###    namedefaultc                     t           j                            | d                                          }|s|S 	 t	          |          S # t
          t          f$ r |cY S w xY w)Nr   )osenvirongetstripfloat	TypeError
ValueError)r   r   raws      r   
_float_envr%   -   sj    
*..r
"
"
(
(
*
*C Szzz"   s   A AAreply_to_message_idc                 2   t          | dd          }|dS d|i}t          t          | dd                    dk    r[t          | dd          dk    rFd|d<   t          |          }|r	|d	vr||d
<   |pt          | dd          }|t          |          |d<   |S )a  Build platform-aware thread metadata for adapter sends.

    Most platforms route threaded sends with a generic ``thread_id`` metadata
    value. Telegram private-chat topics created through Hermes' DM-topic helper
    are exposed in updates as ``message_thread_id`` plus a reply anchor. Live
    user-message replies route with ``message_thread_id`` + ``reply_to_message_id``;
    synthetic/resumed sends that have no reply anchor fall back to Telegram's
    ``direct_messages_topic_id`` when the Bot API supports it.
    	thread_idNr   telegram	chat_typedmT telegram_dm_topic_reply_fallback>   r   1direct_messages_topic_id
message_idtelegram_reply_to_message_id)r   r   r   )sourcer&   r(   metadatatidanchors         r   _thread_metadata_for_sourcer5   7   s     T22ItY'Hgfj$7788JFF7SY[fhlKmKmquKuKu7;34)nn 	73i''36H/0$Kd(K(K7:6{{H34Or   c                    t          | dd          }t          t          |dd                    }t          |dd          }|dk    r9|r7t          |dd          dk    r"t          | dd          pt          | d	d          S |dk    r|rdS |d
k    r$|r"t          | d	d          rt          | d	d          S t          | dd          S )a  Return reply_to id for platforms that need reply semantics.

    Telegram forum/supergroup topics should be routed by topic metadata, not by
    replying to the triggering message. Hermes-created Telegram private-chat
    topic lanes prefer replying to the triggering user message so the answer
    stays attached to the active lane; synthetic/resumed sends fall back to
    ``direct_messages_topic_id`` metadata when no message id is available.
    r1   Nr   r(   r)   r*   r+   r/   r&   feishu)r   r   )eventr1   r   r(   s       r   _reply_anchor_for_eventr9   P   s     UHd++Fgfj$??@@HT22I:)T0R0RVZ0Z0Z ulD11`WUDY[_5`5``:)t8	ge=RTX.Y.Yu3T:::5,---r   Fextis_voicec                     |pd                                 }|t          vrdS t          |           dk    r|t          v r|S |t          v S dS )a  Return True when a media file should use the platform's audio sender.

    Other platforms: every recognized audio extension routes through the
    audio sender.

    Telegram: the Bot API only accepts MP3/M4A for sendAudio and
    Opus/OGG for sendVoice. Opus/OGG is only routed as audio when the
    caller flagged ``is_voice=True`` (so we don't turn a regular audio
    attachment into a voice bubble just because the file happens to be
    Opus). Everything else falls through to document delivery by
    returning ``False``.
    r   Fr)   T)r   _AUDIO_EXTSr   _TELEGRAM_VOICE_EXTS_TELEGRAM_AUDIO_ATTACHMENT_EXTS)r   r:   r;   normalized_exts       r   should_send_media_as_audiorA   g   s^     iR&&((N[((uh:--111O!@@@4r   sc                 L    t          |                     d                    dz  S )u  Count UTF-16 code units in *s*.

    Telegram's message-length limit (4 096) is measured in UTF-16 code units,
    **not** Unicode code-points.  Characters outside the Basic Multilingual
    Plane (emoji like 😀, CJK Extension B, musical symbols, …) are encoded as
    surrogate pairs and therefore consume **two** UTF-16 code units each, even
    though Python's ``len()`` counts them as one.

    Ported from nearai/ironclaw#2304 which discovered the same discrepancy in
    Rust's ``chars().count()``.
    z	utf-16-le   )lenencode)rB   s    r   	utf16_lenrG   ~   s#     qxx$$%%**r   limitc                     t          |           |k    r| S dt          |           }}||k     r4||z   dz   dz  }t          | d|                   |k    r|}n|dz
  }||k     4| d|         S )u   Return the longest prefix of *s* whose UTF-16 length ≤ *limit*.

    Unlike a plain ``s[:limit]``, this respects surrogate-pair boundaries so
    we never slice a multi-code-unit character in half.
    r      rD   N)rG   rE   )rB   rH   lohimids        r   _prefix_within_utf16_limitrN      s     ||uAB
r''Bw{q QttW&&BBqB r'' SbS6Mr   budgetc                      ||           |k    rt          |           S dt          |           }}||k     r0||z   dz   dz  } || d|                   |k    r|}n|dz
  }||k     0|S )a8  Return the largest codepoint offset *n* such that ``len_fn(s[:n]) <= budget``.

    Used by :meth:`BasePlatformAdapter.truncate_message` when *len_fn* measures
    length in units different from Python codepoints (e.g. UTF-16 code units).
    Falls back to binary search which is O(log n) calls to *len_fn*.
    r   rJ   rD   NrE   )rB   rO   len_fnrK   rL   rM   s         r   _custom_unit_to_cprS      s     vayyF1vvAB
r''Bw{q 6!DSD'??f$$BBqB r'' Ir   hostc                    	 t          j        |           }|j        rdS t          |dd          r|j        j        rdS dS # t
          $ r Y nw xY w	 t          j        | dt          j        t          j	                  }|D ],\  }}}}}t          j        |d                   }|j        s dS -dS # t          j
        t          f$ r Y dS w xY w)a  Return True if *host* would expose the server beyond loopback.

    Loopback addresses (127.0.0.1, ::1, IPv4-mapped ::ffff:127.0.0.1)
    are local-only.  Unspecified addresses (0.0.0.0, ::) bind all
    interfaces.  Hostnames are resolved; DNS failure fails closed.
    Fipv4_mappedNTr   )	ipaddress
ip_addressis_loopbackr   rV   r#   _socketgetaddrinfo	AF_UNSPECSOCK_STREAMgaierrorOSError)rT   addrresolved_family_type_proto
_canonnamesockaddrs           r   is_network_accessiblerg      s   #D)) 	5 4-- 	$2B2N 	5t   &$)7+>
 

 =E 	 	8GUFJ'44D# ttug&   tts/   A  A   
AAAB- *B- -CCc                  2   t           j        dk    rdS 	 t          j        ddgddt          j                  } n# t
          $ r Y dS w xY wi }|                                 D ]\}|                                }d|v rB|                    d          \  }}}|                                ||                                <   ]d	D ]W\  }}}|	                    |          d
k    r8|	                    |          }	|	                    |          }
|	r|
r
d|	 d|
 c S XdS )zRead the macOS system HTTP(S) proxy via ``scutil --proxy``.

    Returns an ``http://host:port`` URL string if an HTTP or HTTPS proxy is
    enabled, otherwise *None*.  Falls back silently on non-macOS or on any
    subprocess error.
    darwinNscutilz--proxy   T)timeouttextstderrz : ))HTTPSEnable
HTTPSProxy	HTTPSPort)
HTTPEnable	HTTPProxyHTTPPortr-   zhttp://:)
sysr   
subprocesscheck_outputDEVNULL	Exception
splitlinesr    	partitionr   )outpropslinekey_val
enable_keyhost_keyport_keyrT   ports              r   _detect_macos_system_proxyr      sU    |xt%y!14
@R
 
 
    tt E   - -zz||D==..//KCC!$E#))+++ / /&
Hh 99Z  C''99X&&D99X&&D / /........4s   $9 
AAr   c                    t          | pd                                          }|sdS d|v rDt          |          }|j        pd                                                    d          |j        fS |                    d          rd|v r|dd                              d          \  }}}d }|                    d          r3|dd          	                                rt          |dd                    }|                                                    d          |fS |                    d          dk    rc|                    d          \  }}}|	                                r6|                                                    d          t          |          fS |                                                    d	                              d          d fS )
Nr   )r   N://.[]rJ   ru   z[])r   r    r   hostnamer   rstripr   
startswithr|   isdigitintcount
rpartition)r   r$   parsedrT   r   restr   
maybe_ports           r   _split_host_portr      s   
ekr


 
 
"
"C x||#%2,,..55c::FKGG
~~c .sczzABB))#..a??3 	!DH$4$4$6$6 	!tABBx==Dzz||""3''--
yy~~!nnS11a 	=::<<&&s++S__<<99;;T""))#..44r   c                      g } dD ]T}t           j                            |d          }|                     d |                    d          D                        U| S )N)NO_PROXYno_proxyr   c              3   f   K   | ],}|                                 |                                 V  -d S Nr    ).0parts     r   	<genexpr>z$_no_proxy_entries.<locals>.<genexpr>  s7      OO$**,,Otzz||OOOOOOr   ,)r   r   r   extendsplit)entriesr   r$   s      r   _no_proxy_entriesr     s`    G' P PjnnS"%%OO		#OOOOOOONr   entryr   c                 $   t          | pd                                                                          }|sdS |dk    rdS t          |          \  }}|
|||k    rdS ||dS |sdS 	 t	          j        |d          }	 t	          j        |          |v S # t          $ r Y dS w xY w# t          $ r Y nw xY w	 t	          j        |          }	 t	          j        |          |k    S # t          $ r Y dS w xY w# t          $ r Y nw xY w|                    d          r|dd          }|	                    |          S |                    d          r#||dd          k    p|	                    |          S ||k    p|	                    d|           S )	Nr   F*Tstrict*.rJ   r   )
r   r    r   r   rW   
ip_networkrX   r#   r   endswith)	r   rT   r   token
token_host
token_portnetworktoken_ipsuffixs	            r   _no_proxy_entry_matchesr     s   ""$$**,,E u||t-e44J
$"2zT7I7Iu$,u u&z%@@@	'--88 	 	 	55	   '
33	'--99 	 	 	55	    T"" %ABB}}V$$$S!! Cz!""~%Bz)B)BB:@/?:/?/?!@!@@s`   +B) B 
B&"B) %B&&B) )
B65B6:C8 C' '
C51C8 4C55C8 8
DDtarget_hosts.c                    t                      }|r| sdS t          | t                    r| g}nt          |           }|D ]C}t	          t          |                    \  s$t          fd|D                       r dS DdS )zReturn True when NO_PROXY/no_proxy matches at least one target host.

    Supports exact hosts, domain suffixes, wildcard suffixes, IP literals,
    CIDR ranges, optional host:port entries, and ``*``.
    Fc              3   :   K   | ]}t          |          V  d S r   )r   )r   r   rT   r   s     r   r   z&should_bypass_proxy.<locals>.<genexpr>P  s0      OOe&udD99OOOOOOr   T)r   
isinstancer   listr   any)r   r   
candidates	candidaterT   r   s       @@r   should_bypass_proxyr   ?  s      !!G , u,$$ ("^

,''
  	%c)nn55
d 	OOOOOwOOOOO 	44	5r   )r   platform_env_varc                   | rUt           j                            |           pd                                }|r t	          |          rdS t          |          S dD ]Z}t           j                            |          pd                                }|r#t	          |          r dS t          |          c S [t          t                                }|rt	          |          rdS |S )u  Return a proxy URL from env vars, or macOS system proxy.

    Check order:
      0. *platform_env_var* (e.g. ``DISCORD_PROXY``) — highest priority
      1. HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants)
      2. macOS system proxy via ``scutil --proxy`` (auto-detect)

    Returns *None* if no proxy is found, or if NO_PROXY/no_proxy matches one
    of ``target_hosts``.
    r   N)HTTPS_PROXY
HTTP_PROXY	ALL_PROXYhttps_proxy
http_proxy	all_proxy)r   r   r   r    r   r   r   )r   r   r   r   detecteds        r   resolve_proxy_urlr   U  s     . 0117R>>@@ 	."<00 t&u---: . .$$*1133 	."<00 tt&u-----	. ##=#?#?@@H '55 tOr   	proxy_urlc                     | si S |                                                      d          rO	 ddlm} |                    | d          }d|iS # t
          $ r  t                              d|            i cY S w xY wd| iS )	u  Build kwargs for ``commands.Bot()`` / ``discord.Client()`` with proxy.

    Returns:
      - SOCKS URL  → ``{"connector": ProxyConnector(..., rdns=True)}``
      - HTTP URL   → ``{"proxy": url}``
      - *None*     → ``{}``

    ``rdns=True`` forces remote DNS resolution through the proxy — required
    by many SOCKS implementations (Shadowrocket, Clash) and essential for
    bypassing DNS pollution behind the GFW.
    socksr   ProxyConnectorTrdns	connectorV   aiohttp_socks not installed — SOCKS proxy %s ignored. Run: pip install aiohttp-socksproxy)r   r   aiohttp_socksr   from_urlImportErrorloggerwarningr   r   r   s      r   proxy_kwargs_for_botr   w  s      	##G,, 	444444&//	/EEI++ 	 	 	NN1  
 III	 Ys    A 'A87A8c                    | si i fS 	 ddl m} |                    | d          }d|ii fS # t          $ rQ |                                                     d          r!t                              d|            i i fcY S i d| ifcY S w xY w)	u  Build kwargs for standalone ``aiohttp.ClientSession`` with proxy.

    Returns ``(session_kwargs, request_kwargs)`` where:
      - With aiohttp-socks → ``({"connector": ProxyConnector(...)}, {})``
        for *all* proxy schemes (SOCKS **and** HTTP/HTTPS).
      - HTTP without aiohttp-socks → ``({}, {"proxy": url})``.
      - None → ``({}, {})``.

    Prefer the connector path: it works transparently with libraries
    (like mautrix) that call ``session.request()`` without forwarding
    per-request ``proxy=`` kwargs.

    Usage::

        sess_kw, req_kw = proxy_kwargs_for_aiohttp(proxy_url)
        async with aiohttp.ClientSession(**sess_kw) as session:
            async with session.get(url, **req_kw) as resp:
                ...
    r   r   Tr   r   r   r   r   )r   r   r   r   r   r   r   r   r   s      r   proxy_kwargs_for_aiohttpr     s    (  2v(000000"++ID+AA	Y'++ ( ( (??''00 	NN1  
 r6MMMGY'''''(s   "+ AB=BBr   no_proxy_valuec                 :   |}|@t           j                            d          p t           j                            d          pd}|                                }|sdS |                                 }t          j        d|          D ]}|                                                                }|s+|dk    r dS |                    d	          r|d
d         }n|                    d          r
|dd         }||k    s|                    d|           r dS dS )zReturn True when ``hostname`` matches a ``NO_PROXY`` entry.

    Supports comma- or whitespace-separated entries with optional leading dots
    and ``*.`` wildcards, which match both the apex domain and subdomains.
    Nr   r   r   Fz[\s,]+r   Tr   rD   r   rJ   )	r   r   r   r    r   rer   r   r   )r   r   r$   lower_hostnamer   
normalizeds         r   is_host_excluded_by_no_proxyr     s5    C
{jnnZ((LBJNN:,F,FL"
))++C u^^%%N)S))  [[]]((**
 	44  && 	(#ABBJJ""3'' 	(#ABBJZ''>+B+BCSzCSCS+T+T'44 ( 5r   )	dataclassfield)datetimePath)DictListOptionalAnyCallable	AwaitableTupleUnion)EnumrD   )PlatformPlatformConfig)SessionSourcebuild_session_key)get_default_hermes_rootget_hermes_dirget_hermes_homezSecure secret entry is not supported over messaging. Load this skill in the local CLI to be prompted, or add the key to ~/.hermes/.env manually.P   urlmax_lenc                    |dk    rdS | dS t          |           }|sdS 	 t          |          }n# t          $ r |d|         cY S w xY w|j        rs|j        rl|j                            dd          d         }|j         d| }|j        pd}|r1|dk    r+|                    dd          d         }|r| d	| n| d
}n|}n|}t          |          |k    r|S |dk    rd|z  S |d|dz
            dS )z?Return a URL string safe for logs (no query/fragment/userinfo).r   r   N@rJ   r   /z/.../z/...rk   r   ...)r   r   rz   schemenetlocrsplitpathrE   )	r   r   r$   r   r   baser  basenamesafes	            r   safe_url_for_logr    sc   !||r
{r
c((C r#   8G8} }  %%c1--b1-,,F,,{ b 	DCKK{{3**2.H/7Jd+++++]]]DDDD
4yyG!||W}<GaK< %%%%s   1 AAc                    K   | j         rP| j        rKt          | j        j                  }ddlm}  ||          s#t          dt          |                     dS dS dS )a%  Re-validate each redirect target to prevent redirect-based SSRF.

    Without this, an attacker can host a public URL that 302-redirects to
    http://169.254.169.254/ and bypass the pre-flight is_safe_url() check.

    Must be async because httpx.AsyncClient awaits response event hooks.
    r   is_safe_urlz.Blocked redirect to private/internal address: N)is_redirectnext_requestr   r   tools.url_safetyr  r#   r  )responseredirect_urlr  s      r   _ssrf_redirect_guardr    s         5 80455000000{<(( 	aAQR^A_A_aa  	   	 	r   zcache/imagesimage_cachec                  H    t                               dd           t           S )zBReturn the image cache directory, creating it if it doesn't exist.Tparentsexist_ok)IMAGE_CACHE_DIRmkdir r   r   get_image_cache_dirr  4  !    $666r   datac                    t          |           dk     rdS | dd         dk    rdS | dd         dk    rdS | dd	         d
v rdS | dd         dk    rdS | dd         dk    r#t          |           dk    r| dd         dk    rdS dS )zDReturn True if *data* starts with a known image magic-byte sequence.   FN   s   PNG

Trk   s      >      GIF87a   GIF89arD   s   BMs   RIFF   s   WEBPrQ   )r  s    r   _looks_like_imager!  :  s    
4yy1}}uBQBx'''tBQBx?""tBQBx)))tBQBx5tBQBx7s4yyB4":3H3Ht5r   .jpgc                 B   t          |           s5| dd                             dd          }t          d| d|d          t                      }d	t	          j                    j        dd
          | }||z  }|                    |            t          |          S )a  
    Save raw image bytes to the cache and return the absolute file path.

    Args:
        data: Raw image bytes.
        ext:  File extension including the dot (e.g. ".jpg", ".png").

    Returns:
        Absolute path to the cached image file as a string.

    Raises:
        ValueError: If *data* does not look like a valid image (e.g. an HTML
            error page returned by the upstream server).
    Nr   zutf-8replace)errorsz$Refusing to cache non-image data as z (starts with: )img_r   )	r!  decoder#   r  uuiduuid4hexwrite_bytesr   )r  r:   snippet	cache_dirfilenamefilepaths         r   cache_image_from_bytesr1  K  s     T"" 
ss)""79"==*3 * *$* * *
 
 	
 $%%I2djll&ss+2S22H8#Hx==r   retriesc                 P  K   ddl m}  ||           st          dt          |                      ddl}t          j        t                    }|                    dddt          gi          4 d{V 	 }t          |d	z             D ]}	 |                    | d
dd           d{V }|                                 t          |j        |          c cddd          d{V  S # |j        |j        f$ r}	t#          |	|j                  r|	j        j        dk     r ||k     rQd|d	z   z  }
|                    d|d	z   |t          |           |
|	           t+          j        |
           d{V  Y d}	~	 d}	~	ww xY w	 ddd          d{V  dS # 1 d{V swxY w Y   dS )a@  
    Download an image from a URL and save it to the local cache.

    Retries on transient failures (timeouts, 429, 5xx) with exponential
    backoff so a single slow CDN response doesn't lose the media.

    Args:
        url: The HTTP/HTTPS URL to download from.
        ext: File extension including the dot (e.g. ".jpg", ".png").
        retries: Number of retry attempts on transient failures.

    Returns:
        Absolute path to the cached image file as a string.

    Raises:
        ValueError: If the URL targets a private/internal network (SSRF protection).
    r   r  &Blocked unsafe URL (SSRF protection): Nr   Tr  rl   follow_redirectsevent_hooksrJ   )Mozilla/5.0 (compatible; HermesAgent/1.0)zimage/*,*/*;q=0.8z
User-AgentAcceptheaders        ?z*Media cache retry %d/%d for %s (%.1fs): %s)r  r  r#   r  httpxlogging	getLogger__name__AsyncClientr  ranger   raise_for_statusr1  contentTimeoutExceptionHTTPStatusErrorr   r  status_codedebugasynciosleepr   r:   r2  r  r?  _logclientattemptr  excwaits              r   cache_image_from_urlrS  g       $ -,,,,,;s [YBRSVBWBWYYZZZLLLX&&D  "6!78 !           
Wq[)) 	 	G!'&Q"5  ", " "       ))+++-h.>DDDD               *E,AB   c5#899 cl>VY\>\>\W$$'A+.DJJD!(--   "----------HHHH	                             D   7FAC*F*E>;A8E93F8E99E>>F
F"F   max_age_hoursc                 >   ddl }t                      } |j                     | dz  z
  }d}|                                D ]^}|                                rH|                                j        |k     r+	 |                                 |dz  }N# t          $ r Y Zw xY w_|S )zd
    Delete cached images older than *max_age_hours*.

    Returns the number of files removed.
    r   N  rJ   )timer  iterdiris_filestatst_mtimeunlinkr_   rW  rZ  r.  cutoffremovedfs         r   cleanup_image_cacherd    s     KKK#%%ITY[[MD01FG    99;; 	16688,v55


1   N   2B
BBzcache/audioaudio_cachec                  H    t                               dd           t           S )zBReturn the audio cache directory, creating it if it doesn't exist.Tr  )AUDIO_CACHE_DIRr  r  r   r   get_audio_cache_dirri    r  r   c                     t                      }dt          j                    j        dd          | }||z  }|                    |            t          |          S )a  
    Save raw audio bytes to the cache and return the absolute file path.

    Args:
        data: Raw audio bytes.
        ext:  File extension including the dot (e.g. ".ogg", ".mp3").

    Returns:
        Absolute path to the cached audio file as a string.
    audio_Nr   )ri  r)  r*  r+  r,  r   r  r:   r.  r/  r0  s        r   cache_audio_from_bytesrm    s]     $%%I4
("-4s44H8#Hx==r   c                 P  K   ddl m}  ||           st          dt          |                      ddl}t          j        t                    }|                    dddt          gi          4 d{V 	 }t          |d	z             D ]}	 |                    | d
dd           d{V }|                                 t          |j        |          c cddd          d{V  S # |j        |j        f$ r}	t#          |	|j                  r|	j        j        dk     r ||k     rQd|d	z   z  }
|                    d|d	z   |t          |           |
|	           t+          j        |
           d{V  Y d}	~	 d}	~	ww xY w	 ddd          d{V  dS # 1 d{V swxY w Y   dS )aE  
    Download an audio file from a URL and save it to the local cache.

    Retries on transient failures (timeouts, 429, 5xx) with exponential
    backoff so a single slow CDN response doesn't lose the media.

    Args:
        url: The HTTP/HTTPS URL to download from.
        ext: File extension including the dot (e.g. ".ogg", ".mp3").
        retries: Number of retry attempts on transient failures.

    Returns:
        Absolute path to the cached audio file as a string.

    Raises:
        ValueError: If the URL targets a private/internal network (SSRF protection).
    r   r  r4  Nr   Tr  r5  rJ   r8  zaudio/*,*/*;q=0.8r9  r;  r=  r>  z*Audio cache retry %d/%d for %s (%.1fs): %s)r  r  r#   r  r?  r@  rA  rB  rC  r  rD  r   rE  rm  rF  rG  rH  r   r  rI  rJ  rK  rL  rM  s              r   cache_audio_from_urlro    rT  rU  zcache/videosvideo_cache	video/mp4zvideo/quicktimez
video/webmzvideo/x-matroskazvideo/x-msvideo).mp4.mov.webm.mkv.avic                  H    t                               dd           t           S )zBReturn the video cache directory, creating it if it doesn't exist.Tr  )VIDEO_CACHE_DIRr  r  r   r   get_video_cache_dirry  &  r  r   rr  c                     t                      }dt          j                    j        dd          | }||z  }|                    |            t          |          S )zDSave raw video bytes to the cache and return the absolute file path.video_Nr   )ry  r)  r*  r+  r,  r   rl  s        r   cache_video_from_bytesr|  ,  s[    #%%I4
("-4s44H8#Hx==r   zcache/documentsdocument_cachezcache/screenshotsbrowser_screenshotsHERMES_MEDIA_ALLOW_DIRSHERMES_MEDIA_TRUST_RECENT_FILES!HERMES_MEDIA_TRUST_RECENT_SECONDSHERMES_MEDIA_DELIVERY_STRICTcacheimagesaudiovideos	documentsscreenshotsiX  )	z/etcz/procz/sysz/devz/rootz/bootz/var/logz/var/libz/var/run)	z.sshz.awsz.gnupgz.kubez.dockerz.configz.azurez.gcloudzLibrary/Keychainsc                     d t           D             } t          j                            t          d          }|                    t          j                  D ]}|                    d          D ]n}|                                }|st          t          j	        
                    |                    }|                                r|                     |           o| S )zCReturn roots from which model-emitted local media may be delivered.c                 ,    g | ]}t          |          S r  r   )r   roots     r   
<listcomp>z1_media_delivery_allowed_roots.<locals>.<listcomp>  s    >>>DT$ZZ>>>r   r   r   )MEDIA_DELIVERY_SAFE_ROOTSr   r   r   MEDIA_DELIVERY_ALLOW_DIRS_ENVr   pathsepr    r   r  
expanduseris_absoluteappend)rootsextra_rootschunkraw_rootr  s        r   _media_delivery_allowed_rootsr    s    >>$=>>>E*..!>CCK""2:.. # #C(( 	# 	#H~~''H **84455D!! #T"""	# Lr   c                     t           j                            t          d                                                                          } | dv rdS 	 t           j                            t          d                                          }|rt          |          }t          d|          S n# t          t          f$ r Y nw xY wt          t                    S )zReturn the recency window for trusting freshly-produced files.

    0 disables recency-based trust entirely (pure-allowlist mode).
    r-   )0falsenooffr           r   )r   r   r   MEDIA_DELIVERY_TRUST_RECENT_ENVr    r   'MEDIA_DELIVERY_TRUST_RECENT_SECONDS_ENVr!   maxr"   r#   ,_MEDIA_DELIVERY_TRUST_RECENT_DEFAULT_SECONDS)r$   customsecondss      r   _media_delivery_recency_secondsr    s    
 *..8#
>
>
D
D
F
F
L
L
N
NC
---s GLLRRTT 	%FmmGsG$$$	% z"   =>>>s   AB* *B>=B>c                      t           j                            t          d                                                                          } | dv S )u"  Return True when path validation should require allowlist/recency match.

    Off by default. In non-strict mode, ``validate_media_delivery_path``
    accepts any existing regular file that isn't under the credential /
    system-path denylist — restoring the pre-#29523 behavior for the
    single-user case. Strict mode preserves the original
    allowlist+recency-window logic for operators running public-facing
    gateways where prompt injection from one user shouldn't be able to
    exfiltrate the host's secrets to that same user.
    r  )r-   trueyeson)r   r   r   MEDIA_DELIVERY_STRICT_ENVr    r   )r$   s    r   _media_delivery_strict_moder    s>     *..2C
8
8
>
>
@
@
F
F
H
HC,,,r   c                     d t           D             } t          t          j                            d                    }t
          D ]}|                     ||z             t          t          fD ]b}|                     |dz             |                     |dz             |                     |dz             |                     |dz             c| S )zEReturn absolute denylist paths under which delivery is never allowed.c                 ,    g | ]}t          |          S r  r   )r   ps     r   r  z0_media_delivery_denied_paths.<locals>.<listcomp>  s    ???!d1gg???r   ~z.envz	auth.jsoncredentialszconfig.yaml)	_MEDIA_DELIVERY_DENIED_PREFIXESr   r   r  r  $_MEDIA_DELIVERY_DENIED_HOME_SUBPATHSr  _HERMES_HOME_HERMES_ROOT)deniedhomesubhermes_roots       r   _media_delivery_denied_pathsr    s    ??>???F""3''((D3 " "dSj!!!! %l3 3 3kF*+++kK/000kM1222kM12222Mr   ra   c                    	 t          t          j                            d                                        d          }n# t
          t          t          f$ r d}Y nw xY wt                      D ]k}	 |                                                    d          }n# t
          t          t          f$ r Y Ew xY wt          | |          s| |k    s`|||k    ri dS dS )u2  Return True if ``resolved`` lives under a deny-listed system path.

    One narrow exception: when a denied prefix IS the running user's own home,
    the home itself is not treated as denied. ``/root`` is on the system-path
    denylist so that a non-root gateway can't deliver another user's home, but
    on a root-run gateway ``$HOME=/root`` and the operator's own deliverables
    (``/root/work/proposal.docx``) live directly under it. The credential
    sub-directories inside home (``~/.ssh``, ``~/.aws``, ...) and Hermes
    secrets (``~/.hermes/.env``, ``auth.json``) are *separate, more-specific*
    denied paths, so they stay blocked regardless of this exception — it can
    only un-block a plain file sitting in the running user's home tree, never a
    credential location or another user's home.
    r  Fr   NT)
r   r   r  r  resolver_   RuntimeErrorr#   r  _path_is_within)ra   r  r  resolved_denieds       r   _path_under_denied_prefixr    s   BG&&s++,,44E4BB\:.   .00  	$//11999GGOOz2 	 	 	H	/:: 	h/>Y>Y 4 7 7tt5s$   A A AA3(BB65B6window_secondsc                     |dk    rdS 	 |                                  j        }n# t          $ r Y dS w xY wt          j                    |z
  |k    S )a^  Return True if the file's mtime is within ``window_seconds`` of now.

    Used as a session-scoped trust signal: agents almost always produce
    delivery artifacts within seconds of asking to send them, while
    prompt-injection paths pointing at pre-existing host files (/etc/passwd,
    ~/.ssh/id_rsa) have mtimes measured in days or months.
    r   F)r]  r^  r_   rZ  )ra   r  mtimes      r   _file_is_recently_producedr    sc     u(   uuIKK%N22s   $ 
22r  r  c                 T    	 |                      |           dS # t          $ r Y dS w xY w)NTF)relative_tor#   )r  r  s     r   r  r    sB    t   uus    
''c                 l   | sdS t          |                                           }t          |          dk    r8|d         |d         k    r&|d         dv r|dd                                         }|                    d                              d          }|sdS 	 t          t          j                            |                    }n# t          t          t          f$ r Y dS w xY w|                                sdS 	 |                    d	          }n# t          t          t          f$ r Y dS w xY w|                                sdS t                      D ]j}	 |                                                    d
	          }n# t          t          t          f$ r Y Ew xY wt!          ||          rt          |          c S kt#                      s t%          |          rdS t          |          S t'                      }|dk    r.t%          |          st)          ||          rt          |          S dS )u  Return a safe absolute file path for native media delivery, else None.

    Default mode (single-user / private gateway): accept any existing regular
    file that isn't under the credential / system-path denylist
    (``_MEDIA_DELIVERY_DENIED_PREFIXES`` + ``~/.ssh``, ``~/.aws``, etc.).
    This matches the symmetry of inbound delivery — Telegram/Discord/Slack
    will hand the agent any file the user uploads, and the agent can hand
    back any file that isn't a credential.

    Strict mode (opt-in via ``gateway.strict`` in ``config.yaml`` or
    ``HERMES_MEDIA_DELIVERY_STRICT=1``): the file MUST live under a
    Hermes-managed cache, under an operator-allowlisted root
    (``HERMES_MEDIA_ALLOW_DIRS``), or be freshly produced inside the
    configured recency window. Suitable for public-facing bots where
    prompt injection from one user shouldn't be able to exfiltrate the
    host's secrets to that same user.

    Symlinks are resolved before any containment / denylist check.
    NrD   r   r   `"'rJ   
`"',.;:)}]Tr   F)r   r    rE   lstripr   r   r   r  r  r_   r  r#   r  r  r\  r  r  r  r  r  r  )r  r   expandedra   r  resolved_rootwindows          r   validate_media_delivery_pathr    sm   (  tD		!!I
9~~y|y}<<1QWAWAWadO))++	  ((//>>I t**95566\:.   tt !! t##4#00\:.   tt  t .// ! !	 OO--55U5CCMMz2 	 	 	H	8]33 	!x==   	! '(( $X.. 	48}} -..Fzz3H==z%h77 	!x== 4s6   ,C C&%C& D D21D2(FFFz[\x00-\x1f\x7f\x85\u2028\u2029]c                 b    t                               dt          |                     dd         S )z9Return a single-line, length-bounded path for log output.?N   )_LOG_UNSAFE_CHARSr  r   r  s    r   _log_safe_pathr  X  s'      c$ii00#66r   .pdfzapplication/pdf.mdztext/markdown.txtz
text/plain.csvztext/csvz.log.jsonzapplication/json.xmlzapplication/xml.yamlzapplication/yaml.ymlz.tomlzapplication/tomlz.iniz.cfg.zipzapplication/zip.docxzGapplication/vnd.openxmlformats-officedocument.wordprocessingml.document.xlsxzAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet.pptxzIapplication/vnd.openxmlformats-officedocument.presentationml.presentationz.ts)z.pyz.sh
image/jpegz	image/pngz
image/webpz	image/gif)r"  .jpeg.png.webp.gif)4r  r"  r  r  r  z.bmpz.tiffz.svgrr  rs  rv  ru  rt  r	   r   r
   r   r   r   r  r  z.docz.odtz.rtfr  r  z.epubr  z.xlsz.odsr  z.tsvr  r  r  r  r  z.pptz.odpz.keyr  z.tarz.gzz.tgzz.bz2z.xzz.7zz.rarz.apkz.ipaz.htmlz.htmMEDIA_DELIVERY_EXTS|c              #   @   K   | ]}|                     d           V  dS r   Nr  r   es     r   r   r     s,      77aAHHSMM777777r   T)r   reversezf[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/|[A-Za-z]:[/\\])\S+(?:[^\S\n]+\S+)*?\.(?:z))(?=[\s`"',;:)\]}]|$)[`"']?c                  H    t                               dd           t           S )zEReturn the document cache directory, creating it if it doesn't exist.Tr  )DOCUMENT_CACHE_DIRr  r  r   r   get_document_cache_dirr    s!    TD999r   r/  c                    t                      }|rt          |          j        nd}|                    dd                                          }|r|dv rd}dt          j                    j        dd          d| }||z  }|                                	                    |                                          st          d	|          |                    |            t          |          S )
a  
    Save raw document bytes to the cache and return the absolute file path.

    The cached filename preserves the original human-readable name with a
    unique prefix: ``doc_{uuid12}_{original_filename}``.

    Args:
        data: Raw document bytes.
        filename: Original filename (e.g. "report.pdf").

    Returns:
        Absolute path to the cached document file as a string.

    Raises:
        ValueError: If the sanitized path escapes the cache directory.
    document r   >   ..r   doc_Nr   r   zPath traversal rejected: )r  r   r   r$  r    r)  r*  r+  r  is_relative_tor#   r,  r   )r  r/  r.  	safe_namecached_namer0  s         r   cache_document_from_bytesr    s    " '((I'/?X##ZI!!&"--3355I 	[00	<)#2#.<<<<K;&H,,Y->->-@-@AA CAXAABBBx==r   c                 >   ddl }t                      } |j                     | dz  z
  }d}|                                D ]^}|                                rH|                                j        |k     r+	 |                                 |dz  }N# t          $ r Y Zw xY w_|S )zg
    Delete cached documents older than *max_age_hours*.

    Returns the number of files removed.
    r   NrY  rJ   )rZ  r  r[  r\  r]  r^  r_  r_   r`  s         r   cleanup_document_cacher    s     KKK&((ITY[[MD01FG    99;; 	16688,v55


1   Nre  c                   H    e Zd ZU dZeed<   eed<   eed<   eed<   defdZdS )	CachedMediaz)Result of caching one attachment's bytes.r  
media_typekinddisplay_namer   c                 8    d| j          d| j         d| j         dS )z>One-line transcript annotation pointing the agent at the file.r   z 'z' saved at: r   )r  r  r  selfs    r   context_notezCachedMedia.context_note  s*    K49KK 1KKtyKKKKr   N)rB  
__module____qualname____doc__r   __annotations__r  r  r   r   r  r    se         33
IIIOOO
IIILc L L L L L Lr   r  	mime_typec                 .   | r;t           j                            |           d                                         }|r|S |pd                                }|sdS t          t
          t          fD ](}|                                D ]\  }}||k    r|c c S )dS )z=Best-effort file extension from filename, then MIME fallback.rJ   r   )r   r  splitextr   SUPPORTED_IMAGE_DOCUMENT_TYPESSUPPORTED_VIDEO_TYPESSUPPORTED_DOCUMENT_TYPESitems)r/  r  r:   mimetablems         r   _resolve_media_extr  #  s     gx((+1133 	JO""$$D r&   
 kkmm 	 	FCDyy




 	 2r   r   )r/  r  default_kindr  c                   ddl m} t          ||          }|pd                                }|rt	          j        dd|          n|                    d          pd}|                    d          p|t          v p|d	k    }|                    d
          p|t          v p|dk    }	|                    d          p|dk    }
|r~|t          v r|nd}	 t          | |          }n# t          $ r Y dS w xY w|                    d          r|nt                              |d          }t           ||          |d	|          S |	rR|t          v r|nd}t          | |          }t           ||          t                              |d          d|          S |
rc|dv r|nd}t          | |          }|                    d          r|nd|                    d           }t           ||          |d|          S |t           vrdS t#          | |pd|           }t           ||          t           |         d|pd|           S )u  Classify and cache raw attachment bytes; return a CachedMedia or None.

    ``default_kind`` ("image"/"video"/"audio"/"document") biases classification
    when the extension/MIME are ambiguous — e.g. a Telegram native photo whose
    file has no usable name. Unsupported document types return None so the
    caller can record an "unsupported" note. Images that fail validation
    (``cache_image_from_bytes`` raises ValueError) also return None.
    r   )to_agent_visible_cache_pathr   z	[^\w.\- ]r   r   filezimage/imagezvideo/videozaudio/r  r"  )r:   Nr  rr  rq  r   r
   r  )tools.credential_filesr  r  r   r   r  r  r   r
  r  r1  r#   r   r  r|  rm  r  r  )r  r/  r  r  r  r:   r  displayis_imageis_videois_audioimg_extr  out_mimevid_extaud_exts                   r   cache_media_bytesr!  7  s    CBBBBB
Xy
1
1CO""$$D5=^bf\3111CJJsOOD]W]G 	!! 	#00	#7" 
 x((cC3H,HcL\cLcHx((CLG,CH Z >>>##F	)$G<<<DD 	 	 	44	??844s44:X:\:\]dfr:s:s66t<<hQXYYY A 555##6%d88866t<<>S>W>WX_al>m>movx  A  A  	A Z RRR##X^%d888??844X44:X7>>RUCVCV:X:X66t<<hQXYYY
***t$T8+G7G#7G7GHHD22488:RSV:WYcel  fA  qA{~  qA  qA  B  B  Bs   C! !
C/.C/c                   6    e Zd ZdZdZdZdZdZdZdZ	dZ
d	Zd
ZdS )MessageTypezTypes of incoming messages.rm   locationphotor  r  voicer  stickercommandN)rB  r  r  r  TEXTLOCATIONPHOTOVIDEOAUDIOVOICEDOCUMENTSTICKERCOMMANDr  r   r   r#  r#  o  sA        %%DHEEEEHGGGGr   r#  c                       e Zd ZdZdZdZdZdS )ProcessingOutcomez=Result classification for message-processing lifecycle hooks.successfailure	cancelledN)rB  r  r  r  SUCCESSFAILURE	CANCELLEDr  r   r   r3  r3  |  s#        GGGGIIIr   r3  c                      e Zd ZU dZeed<   ej        Zeed<   dZ	e
ed<   dZeed<   dZee         ed<   dZee         ed<    ee	          Zee         ed
<    ee	          Zee         ed<   dZee         ed<   dZee         ed<   dZeeee         z           ed<   dZee         ed<   dZee         ed<   dZeed<    eej        	          Zeed<   defdZ dee         fdZ!defdZ"dS )MessageEventzi
    Incoming message from a platform.
    
    Normalized representation that all adapters produce.
    rm   message_typeNr1   raw_messager/   platform_update_id)default_factory
media_urlsmedia_typesr&   reply_to_text
auto_skillchannel_promptchannel_contextFinternal	timestampr   c                 6    | j                             d          S )z8Check if this is a command message (e.g., /new, /reset).r   )rm   r   r   s    r   
is_commandzMessageEvent.is_command  s    y##C(((r   c                    |                                  sdS | j                            d          }|r"|d         dd                                         nd}|r d|v r|                    dd          d         }|rd|v rdS |S )z2Extract command name if this is a command message.NrJ   maxsplitr   r   r   )rI  rm   r   r   )r  partsr$   s      r   get_commandzMessageEvent.get_command  s       	4	++&+5eAhqrrl  """ 	'3#::))C##A&C 	3#::4
r   c                 (   |                                  s| j        S | j                            d          }t          |          dk    r|d         nd}|                    dd                              dd                              dd          }|S )	z"Get the arguments after a command.rJ   rK  r   u   ——z--u   —u   –-)rI  rm   r   rE   r$  )r  rM  argss      r   get_command_argszMessageEvent.get_command_args  s       	9	++u::>>uQxxr||ND1199(DIIQQRZ\_``r   )#rB  r  r  r  r   r  r#  r)  r<  r1   r   r=  r   r/   r   r>  r   r   r   r@  r   rA  r&   rB  rC  rD  rE  rF  boolr   nowrG  rI  rN  rR  r  r   r   r;  r;    s          III + 0L+000 !FM    K $J$$$ )-,,, "E$777JS	777"U4888Kc888 *.#---#'M8C=''' -1JtCy)000 %)NHSM((( &*OXc]))) Hd  %===Ix===)D ) ) ) )Xc]    #      r   r;  c                   H    e Zd ZU eed<   ej        dz  ed<   eed<   eed<   dS )TextDebounceStater8   Ntaskfirst_tslast_ts)rB  r  r  r;  r  rK  Taskr!   r  r   r   rV  rV    sA         
,
OOONNNNNr   rV  z4^(?:please\s+)?restart\s+(?:the\s+)?gateway[.!?\s]*$z=^(?:please\s+)?restart\s+(?:the\s+)?hermes\s+gateway[.!?\s]*$z(^(?:please\s+)?restart\s+hermes[.!?\s]*$#_PLAINTEXT_GATEWAY_RESTART_PATTERNSr8   c                 f   	 | | j         t          j        k    rdS | j        pd                                }|r|                    d          rdS t          | dd          }t          |dd          dk    rdS t          D ]!}|                    |          r
d| _         dS "dS # t          $ r Y dS w xY w)a  Rewrite a tiny set of DM plaintext admin phrases into slash commands.

    This keeps high-impact operational phrases like ``restart gateway`` out of
    the LLM/tool path, where they can trigger a self-restart from inside the
    currently running agent and leave the gateway stuck in ``draining`` while it
    waits for that same agent to finish.

    Scope is intentionally narrow: DM text messages only, exact restart-style
    phrases only. Group chats keep natural-language semantics.
    Nr   r   r1   r*   r+   z/restart)
r<  r#  r)  rm   r    r   r   r[  matchrz   )r8   rm   r1   patterns       r    coerce_plaintext_gateway_commandr_    s    =E.+2BBBF
 b'')) 	ts++ 	F$//6;--55F: 	 	G}}T"" '
	 	    s(   B" 2B" &B" 7&B" B" "
B0/B0c                   |    e Zd ZU dZeed<   dZee         ed<   dZ	ee         ed<   dZ
eed<   dZeed<   d	Zeed
<   dS )
SendResultzResult of sending a message.r4  Nr/   errorraw_responseF	retryabler  continuation_message_ids)rB  r  r  r  rS  r  r/   r   r   rb  rc  r   rd  re  tupler  r   r   ra  ra    s~         &&MMM $J$$$E8C=L# It ')e(((((r   ra  c                   n     e Zd ZU dZee         ed<   ddedee         f fdZe	defd            Z
 xZS )	EphemeralReplyu  System-notice reply that auto-deletes after a TTL.

    Slash-command handlers in ``gateway/run.py`` can return this wrapper
    instead of a plain string to request that the reply message be deleted
    after ``ttl_seconds`` on platforms that support ``delete_message``.

    Subclassing ``str`` keeps the wrapper transparent to anything that
    treats handler return values as text (existing tests use ``in`` /
    ``startswith`` / equality; the ``_process_message_background`` pipeline
    extracts attachments from the string content).  ``isinstance(r,
    EphemeralReply)`` still distinguishes ephemeral replies from plain
    strings so the send path can schedule deletion.

    Platforms that don't override :meth:`BasePlatformAdapter.delete_message`
    silently ignore the TTL — the message is sent normally and left in
    place.  When ``ttl_seconds`` is ``None``, the pipeline uses the
    configured ``display.ephemeral_system_ttl`` default.  A default of ``0``
    disables auto-deletion globally, preserving prior behavior.
    ttl_secondsNrm   c                 Z    t                                          | |          }||_        |S r   )super__new__ri  )clsrm   ri  instance	__class__s       r   rl  zEphemeralReply.__new__4  s'    77??3--*r   r   c                 6    t                               |           S )zReturn the underlying text.

        Provided for call sites that want an explicit string conversion,
        though ``str(reply)`` and using ``reply`` directly where a string
        is expected both work identically.
        )r   __str__r   s    r   rm   zEphemeralReply.text9  s     {{4   r   r   )rB  r  r  r  r   r   r  r   rl  propertyrm   __classcell__)ro  s   @r   rh  rh    s          ( # 3 Xc]      
 !c ! ! ! X! ! ! ! !r   rh  
merge_textpending_messagessession_keyru  c                j   |                      |          }|rt          |dd          t          j        k    }|j        t          j        k    }t          |j                  }t          |j                  }|rs|rq|j                            |j                   |j                            |j                   |j	        r*t                              |j	        |j	                  |_	        dS |s|r|r>|j                            |j                   |j                            |j                   |j	        r>|j	        r+t                              |j	        |j	                  |_	        n|j	        |_	        |s|rt          j        |_        n@t          |dd          t          j        k    r!|j        t          j        k    r|j        |_        dS |rat          |dd          t          j        k    rB|j        t          j        k    r-|j	        r$|j	        r|j	         d|j	         n|j	        |_	        dS || |<   dS )a  Store or merge a pending event for a session.

    Photo bursts/albums often arrive as multiple near-simultaneous PHOTO
    events. Merge those into the existing queued event so the next turn sees
    the whole burst.

    When ``merge_text`` is enabled, rapid follow-up TEXT events are appended
    instead of replacing the pending turn. This is used for Telegram bursty
    follow-ups so a multi-part user thought is not silently truncated to only
    the last queued fragment.
    r<  N
)r   r   r#  r+  r<  rS  r@  r   rA  rm   BasePlatformAdapter_merge_captionr)  )	rv  rw  r8   ru  existingexisting_is_photoincoming_is_photoexisting_has_mediaincoming_has_medias	            r   merge_pending_message_eventr  D  s=   $  ##K00H &#HndCC{GXX!.+2CC!("566!%"233 	!2 	&&u'7888 ''(9:::z ^ 3 B B8=RWR\ ] ]F 	!3 	! ?#**5+;<<<$++E,=>>>z /= /$7$F$Fx}V[V`$a$aHMM$)JHM  ;$5 ;(3(9%%.$77;;KKK&+*:::(-(:%F 	.$77;;KKK"k&666z bDLM a8= @ @EJ @ @ @W\WaF$)[!!!r   )	connecterrorconnectionerrorconnectionresetconnectionrefusedconnecttimeoutr   zbroken piperemotedisconnectedeoferrorconfig_extra
channel_id	parent_idc                     |                      d          pi }t          |t                    sdS ||fD ]D}|s|                     |          }|t          |                                          }|r|c S EdS )a  Resolve a per-channel ephemeral prompt from platform config.

    Looks up ``channel_prompts`` in the adapter's ``config.extra`` dict.
    Prefers an exact match on *channel_id*; falls back to *parent_id*
    (useful for forum threads / child channels inheriting a parent prompt).

    Returns the prompt string, or None if no match is found.  Blank/whitespace-
    only prompts are treated as absent.
    channel_promptsN)r   r   dictr   r    )r  r  r  promptsr   prompts         r   resolve_channel_promptr    s     0117RGgt$$ tI&   	S!!>V""$$ 	MMM	4r   c                 ,   |                      d          pg }t          |t                    r|sdS t                      }|r"|                    t          |                     |r"|                    t          |                     |sdS |D ]}t          |t                    st          |                     dd                    }||v r|                     d          p|                     d          }t          |t
                    r|                                }|r|gndc S t          |t                    rT|rRg }	|D ]G}
t          |
t
                    s|
                                }|r||	vr|	                    |           H|	pdc S dS )a  Resolve auto-loaded skill(s) for a channel/thread from platform config.

    Looks up ``channel_skill_bindings`` in the adapter's ``config.extra`` dict.

    Config format::

        channel_skill_bindings:
          - id: "C0123"          # Slack channel ID or Discord channel/forum ID
            skills: ["skill-a", "skill-b"]
          - id: "D0ABCDE"
            skill: "solo-skill"  # single string also accepted

    Prefers an exact match on *channel_id*; falls back to *parent_id*
    (useful for forum threads / Slack threads inheriting the parent channel's
    binding).

    Returns a deduplicated list of skill names (order preserved), or None if
    no match is found.
    channel_skill_bindingsNidr   skillsskill)	r   r   r   setaddr   r  r    r  )r  r  r  bindingsids_to_checkr   entry_idr  rB   seenr   nms               r   resolve_channel_skillsr    s   0  899?RHh%% X t UUL *Z))) )Y((( t $ $%&& 	uyyr**++|##YYx((>EIIg,>,>F&#&& *LLNN)ssT)))&$'' $F $"$" ( (D%dC00 ! B (bnnB|t###4r   rm   c                     | s| S |                      dd                               dd          } t                              d|           S )a  Strip internal delivery directives ([[audio_as_voice]], [[as_document]],
    MEDIA:<path>) so they never render as visible text.

    Backstop only: run ``extract_media`` first. MEDIA cleanup uses the shared
    ``MEDIA_TAG_CLEANUP_RE`` (only tags whose path has a known deliverable
    extension are removed; an unknown-extension tag is intentionally left so the
    bare-path detector downstream can still pick it up, per #34517). [[...]] is
    exact.
    [[audio_as_voice]]r   [[as_document]])r$  MEDIA_TAG_CLEANUP_REr  rm   s    r   _strip_media_directivesr    sL      <<,b1199:KRPPD##B---r   c                      e Zd ZU dZdZeed<   dZeed<   de	de
fdZed	eegef         fd
            Zed	efd            Z	 	 ddee         deeeef                  d	efdZ	 ddedededeeeef                  d	ef
dZdeded	dfdZddddededed	ee         fdZed	efd            Zed	ee         fd            Zed	ee         fd            Zed	efd             Zded	efd!Zd"ed ged         dz  f         d	dfd#Zdd$Z dd%Z!d&ed'ed(ed	dfd)Z"d*ed	dfd+Z#dd,Z$d-ed.ed/ed	efd0Z%dd1Z&ed	efd2            Z'ed	efd3            Z(d"e)d	dfd4Z*d5eeegee         f                  d	dfd6Z+de,d	dfd7Z-d"eee,egee         f                  d	dfd8Z.d9ed	dfd:Z/e0d	efd;            Z1e0dd<            Z2e0	 	 ddeded=ee         deeeef                  d	ef
d>            Z3dZ4eed?<   d@edAed	ee         fdBZ5ddCdedDededEed	ef
dFZ6dedDed	efdGZ7d	efdHZ8dedDedIed	dfdJZ9	 ddedKed'edLedMedeeeef                  d	efdNZ:	 ddedOedPee;         dQedLedeeeef                  d	efdRZ<	 	 ddedSee         ded=ee         deeeef                  d	efdTZ=dded	dfdUZ>ded	dfdVZ?	 	 ddedXe@eAeef                  deeeef                  dYeBd	df
dZZC	 	 	 dded[ed\ee         d=ee         deeeef                  d	efd]ZD	 	 	 dded^ed\ee         d=ee         deeeef                  d	efd_ZEeFd`ed	efda            ZGeFded	eAe@eAeef                  ef         fdb            ZH	 	 	 ddedced\ee         d=ee         deeeef                  d	efddZIdeed	efdfZJdedced	efdgZK	 	 	 ddedhed\ee         d=ee         deeeef                  d	efdiZL	 	 	 	 ddedjed\ee         dkee         d=ee         deeeef                  d	efdlZM	 	 	 ddedmed\ee         d=ee         deeeef                  d	efdnZNeFdoed	ee         fdp            ZOeFd	e@eAeef                  fdq            ZPeFd	e@e         fdr            ZQeFded	efds            ZReFded	efdt            ZSeFded	eAe@eAeef                  ef         fdu            ZTeFded	eAe@e         ef         fdv            ZU	 	 	 ddedxeBdyeVjW        dz  d	dfdzZXded	dfd{ZYded	dfd|ZZdLeded	dfd}Z[dd~dLedededz  d	dfdZ\dd~dLededz  d	edz  fdZ]de,d	dfdZ^de,de_d	dfdZ`dededed	dfdZaeFdee         d	efd            ZbeFdee         d	efd            Zcded	eAee         ef         fdZd	 	 	 	 ddeded=ee         dededeBd	dfdZeeFdee         ded	efd            Zfd	egeehf         fdZide,d	efdZjde,de,d	efdZkdLed	eBfdZldLede,d	dfdZmdLedeBd	dfdZndLed	efdZodLed	dfdZpdddLedeeVjW                 d	dfdZqdLed	efdZrdLed	efdZsddde,dLedeeVjW                 d	efdZtddddLededed	dfdZudLedeVjW        d	dfdZvde,dLeded	dfdZwde,d	dfdZxeFd	eBfd            Zyde,dLed	dfdZzddZ{dLed	efdZ|dLed	ee,         fdZ}	 	 	 	 	 	 	 	 	 	 	 	 	 ddedee         dedSee         dee         dee         dee         dee         dee         dedee         d@ee         dDee         ded	e~fdZe0ded	eeef         fdÄ            Zded	efdĄZeF	 	 ddededed         d	e@e         fdɄ            ZdS )rz  z
    Base class for platform adapters.
    
    Subclasses implement platform-specific logic for:
    - Connecting and authenticating
    - Receiving messages
    - Sending messages/responses
    - Handling media
    Fsupports_code_blocksr   typed_command_prefixconfigr   c                    || _         || _        d | _        d | _        d| _        d | _        d | _        d| _        d | _        i | _	        i | _
        i | _        t          j                            dd                                                                          pd| _        t%          dd          | _        t%          dd          | _        i | _        t-                      | _        i | _        t-                      | _        d | _        d| _        t-                      | _        t-                      | _        t-                      | _        d S )	NFTHERMES_GATEWAY_BUSY_TEXT_MODE	interrupt)HERMES_GATEWAY_BUSY_TEXT_DEBOUNCE_SECONDSgffffff?)HERMES_GATEWAY_BUSY_TEXT_HARD_CAP_SECONDSg      ?)r  r   _message_handler_topic_recovery_fn_running_fatal_error_code_fatal_error_message_fatal_error_retryable_fatal_error_handler_active_sessions_pending_messages_session_tasksr   r   r   r    r   _busy_text_moder%   _busy_text_debounce_seconds_busy_text_hard_cap_seconds_text_debouncer  _background_tasks_post_delivery_callbacks_expected_cancelled_tasks_busy_session_handler_auto_tts_default_auto_tts_enabled_chats_auto_tts_disabled_chats_typing_paused)r  r  r   s      r   __init__zBasePlatformAdapter.__init__"  sA    :> MQ0437!&*#im! ;=:<79 JNN:KHHNNPPVVXX  	 3=73
 3
( 3=73
 3
( =? 58EE 9;%<?EE&_c" (-,/EE$-0UU% $'55r   r   c                     t           S )zReturn the length function for measuring message size on this platform.

        Override in adapters whose platform counts characters differently from
        Python ``len`` (e.g. Telegram counts UTF-16 code units).
        rQ   r   s    r   message_len_fnz"BasePlatformAdapter.message_len_fni  s	     
r   c                     dS )uy  Whether this adapter gates inbound access before dispatch.

        Some adapters (WeCom, Weixin, Yuanbao, QQBot, WhatsApp) implement a
        documented config-driven access surface — ``dm_policy`` / ``group_policy`` /
        ``allow_from`` / ``group_allow_from`` in ``PlatformConfig.extra`` — and
        enforce it at intake: a message is dropped inside the adapter and never
        reaches the gateway unless it already passed that policy.

        The gateway's env-based allowlist check runs *after* the adapter, so for
        these platforms a message arriving at ``_is_user_authorized`` has, by
        definition, already been authorized by the adapter. Without this flag the
        gateway would then deny it again (no env allowlist → default deny),
        silently breaking ``dm_policy: open`` and config-only allowlists.

        Adapters that own their access policy override this to return ``True``.
        The gateway treats that as "already authorized at intake" and skips the
        env-allowlist default-deny. Adapters that delegate access control to the
        gateway leave it ``False`` (the default).
        Fr  r   s    r   enforces_own_access_policyz.BasePlatformAdapter.enforces_own_access_policyr  s	    * ur   Nr*   r2   c                     dS )a  Whether this adapter supports native streaming-draft updates.

        Telegram Bot API 9.5 introduced ``sendMessageDraft``, which renders an
        animated streaming preview as the bot calls it repeatedly with the
        same ``draft_id`` and growing text.  Adapters that implement
        ``send_draft`` should return True here for the chat types where the
        platform supports it (Telegram restricts drafts to private DMs).

        Default implementation returns False.  Stream consumers fall back to
        the edit-based path (``send`` + ``edit_message``) when this returns
        False or when ``send_draft`` raises.
        Fr  )r  r*   r2   s      r   supports_draft_streamingz,BasePlatformAdapter.supports_draft_streaming  s	    " ur   chat_iddraft_idrF  c                 N   K   t          t          |           j         d          )a'  Send or update an animated streaming-draft preview.

        Reuse the same ``draft_id`` (any non-zero int) across consecutive
        calls within a single response so the platform animates the preview
        rather than re-creating it.  Different responses must use different
        ``draft_id`` values within the same chat to avoid animating over a
        prior bubble.

        Drafts have no message_id and cannot be edited, replied to, or
        deleted via normal message APIs.  When the response finishes, the
        caller delivers the final answer as a regular ``send`` and the
        draft preview clears naturally on the client.

        Default implementation raises NotImplementedError; adapters that
        also return True from :meth:`supports_draft_streaming` must override.
        z does not implement send_draft)NotImplementedErrortyperB  )r  r  r  rF  r2   s        r   
send_draftzBasePlatformAdapter.send_draft  s/      . "Dzz"BBB
 
 	
r   r8   sinkc                 L   ddl m}m}m} t	          ||          r%|j        r|                    |j                   dS dS t	          ||          r|j        s|                                 dS dS t	          ||          r#|j        r|	                    |j                   dS dS dS )zRender a MessageChunk / MessageStop / Commentary onto the sink.

        Default: map onto the stream consumer's existing primitives, preserving
        today's behavior 1:1.  ``sink`` is a GatewayStreamConsumer.
        r   )MessageChunkMessageStop
CommentaryN)
gateway.stream_eventsr  r  r  r   rm   on_deltafinalon_segment_breakon_commentary)r  r8   r  r  r  r  s         r   render_message_eventz(BasePlatformAdapter.render_message_event  s     	POOOOOOOOOe\** 	/z *ej)))))* *{++ 	/ ; (%%'''''( (z** 	/z /""5:.....	/ 	// /r   all(   )modepreview_max_lenr  r  c                ~   ddl m} t          ||          sdS ddlm}  ||j        d          }|dk    r|j        rddl}|                    |j        dt          	          }|dk    r#t          |          |k    r|d|d
z
           dz   }| d|j         dt          |j                                                   d| S |j        r| d|j         d|j         dS | d|j         dS |j        }	|	r=|dk    r|nd}
t          |	          |
k    r|	d|
d
z
           dz   }	| d|j         d|	 dS | d|j         dS )a  Return the rendered chrome for a ToolCallChunk, or None to eat it.

        Reproduces the gateway's historical tool-progress formatting: an emoji
        for the tool, the tool name, and a short argument preview (or the full
        args dict in ``verbose`` mode).  Adapters that cannot render tool chrome
        (no message editing, plain-text only) should override to return None so
        the event is dropped rather than spamming separate bubbles.

        ``mode`` is the resolved tool-progress mode ("all" / "new" / "verbose");
        ``preview_max_len`` mirrors the ``tool_preview_length`` config (0 means
        "no cap" in verbose mode).
        r   )ToolCallChunkN)get_tool_emojiu   ⚙️)r   verboseF)ensure_asciir   rk   r    (z)
z: ""r  )r  r  r   agent.displayr  	tool_namerQ  jsondumpsr   rE   r   keyspreview)r  r8   r  r  r  r  emojir  args_strr  caps              r   format_tool_eventz%BasePlatformAdapter.format_tool_event  s    	877777%// 	4000000uAAA9z [::ejuc:RR"Q&&3x==?+J+J'(<1)<(<=EHZZ%/ZZD9J9J4K4KZZPXZZZ} IHH%/HHu}HHHH22eo2222 - 	?%4q%8%8//bC7||c!!!(37(+e3>>eo>>7>>>>..%/....r   c                     | j         d uS r   r  r   s    r   has_fatal_errorz#BasePlatformAdapter.has_fatal_error  s    (44r   c                     | j         S r   r  r   s    r   fatal_error_messagez'BasePlatformAdapter.fatal_error_message	  s    ((r   c                     | j         S r   )r  r   s    r   fatal_error_codez$BasePlatformAdapter.fatal_error_code  s    %%r   c                     | j         S r   )r  r   s    r   fatal_error_retryablez)BasePlatformAdapter.fatal_error_retryable  s    **r   c                 V    || j         v rdS || j        v rdS t          | j                  S )ue  Whether auto-TTS on voice input should fire for ``chat_id``.

        Decision layers (Issue #16007):
          1. Explicit ``/voice on`` or ``/voice tts`` → always fire (even if
             ``voice.auto_tts`` is False).
          2. Explicit ``/voice off`` → never fire.
          3. Fall back to the global ``voice.auto_tts`` config default.
        TF)r  r  rS  r  r  r  s     r   _should_auto_tts_for_chatz-BasePlatformAdapter._should_auto_tts_for_chat  s;     d2224d3335D*+++r   handlerc                     || _         d S r   )r  r  r  s     r   set_fatal_error_handlerz+BasePlatformAdapter.set_fatal_error_handler$  s    $+!!!r   c                 p    d| _         d | _        d | _        d| _        |                     ddd d            d S )NT	connectedplatform_state
error_codeerror_messager  r  r  r  _write_runtime_status_safer   s    r   _mark_connectedz#BasePlatformAdapter._mark_connected'  sF    !%$(!&*#''K\`pt'uuuuur   c                 X    d| _         | j        rd S |                     ddd d            d S )NFdisconnectedr  )r  r  r	  r   s    r   _mark_disconnectedz&BasePlatformAdapter._mark_disconnected.  s>     	F''~bfvz'{{{{{r   codemessagerd  c                p    d| _         || _        || _        || _        |                     dd||           d S )NFfatalr  r  )r  r  r  rd  s       r   _set_fatal_errorz$BasePlatformAdapter._set_fatal_error4  sF    !%$+!&/#''TXho'pppppr   contextc                    	 ddl m}  |dd| j        j        i| dS # t          $ r}t          | dd          }|'t                      }	 || _        n# t          $ r Y nw xY w| j        j        |f}||vr=t          	                    d|| j        j        |           |
                    |           n-t                              d|| j        j        |           Y d}~dS Y d}~dS d}~ww xY w)	a  Write runtime status; log first failure per context at warning, rest at debug.

        Status writes can fail on permissions, ENOSPC, missing status dir, etc.
        A persistently failing status dir used to be silent (``except: pass``).
        Logging every failure would spam the log on reconnect loops, so this
        surfaces the first failure per (platform, context) at warning level and
        downgrades subsequent failures to debug.
        r   )write_runtime_statusr   _status_write_loggedNzPFailed to write runtime status (%s) for %s: %s (further failures at debug level)z.Failed to write runtime status (%s) for %s: %sr  )gateway.statusr  r   r   rz   r   r  r  r   r   r  rJ  )r  r  kwargsr  rQ  loggedr   s          r   r	  z.BasePlatformAdapter._write_runtime_status_safe;  sJ   	r;;;;;;  HH$-*=HHHHHH 	r 	r 	r T#94@@F~06D--    D=&0C&  fT]0#   

3MwX\XeXkmpqqqqqqqqq  !	rs9    
C)!C$
AC$
AC$AA9C$$C)c                 r   K   | j         }|sd S  ||           }t          j        |          r
| d {V  d S d S r   )r  rK  iscoroutine)r  r  results      r   _notify_fatal_errorz'BasePlatformAdapter._notify_fatal_error[  sZ      + 	Fv&& 	LLLLLLLLL	 	r   scopeidentityresource_descc                 f   ddl m} || _        || _         |||d| j        j        i          \  }}|rdS t          |t                    r|                    d          nd}| d|rd	| d
ndz   dz   }t          
                    d| j        |           |                     | d|d           dS )z@Acquire a scoped lock for this adapter. Returns True on success.r   )acquire_scoped_lockr   r2   TpidNz already in usez (PID r&  r   z. Stop the other gateway first.z[%s] %s_lockF)rd  )r  r"  _platform_lock_scope_platform_lock_identityr   r   r   r  r   r   rb  r   r  )	r  r  r  r   r"  acquiredr|  	owner_pidr  s	            r   _acquire_platform_lockz*BasePlatformAdapter._acquire_platform_lockc  s    666666$)!'/$008z4=3F&G
 
 
(  	4+5h+E+EOHLL'''4	---(19$	$$$$r;/0 	
 	Y	7333ooow%HHHur   c                 l    t          | dd          }|sdS ddlm}  || j        |           d| _        dS )z;Release the scoped lock acquired by _acquire_platform_lock.r'  Nr   )release_scoped_lock)r   r  r,  r&  r'  )r  r  r,  s      r   _release_platform_lockz*BasePlatformAdapter._release_platform_lockw  sW    4!:DAA 	F666666D5x@@@'+$$$r   c                 >    | j         j                                        S )z%Human-readable name for this adapter.)r   r   titler   s    r   r   zBasePlatformAdapter.name  s     }"((***r   c                     | j         S )z(Check if adapter is currently connected.)r  r   s    r   is_connectedz BasePlatformAdapter.is_connected  s     }r   c                     || _         dS )z
        Set the handler for incoming messages.
        
        The handler receives a MessageEvent and should return
        an optional response string.
        N)r  r   s     r   set_message_handlerz'BasePlatformAdapter.set_message_handler  s     !(r   fnc                     || _         dS )zInstall a thread_id-recovery hook (Telegram DM topic mode).

        The hook is called with ``event.source`` before session keying;
        a non-None return value replaces ``source.thread_id``. Pass
        ``None`` to clear the hook.
        N)r  )r  r4  s     r   set_topic_recovery_fnz)BasePlatformAdapter.set_topic_recovery_fn  s     #%r   c                    t          | dd          }|dS t          |dd          }|dS 	  ||          }n-# t          $ r  t                              dd           Y dS w xY w|'t	          |          t	          |j        pd          k    rdS 	 t          j        |t	          |                    |_        dS # t          $ r  t                              d	d           Y dS w xY w)
zDRewrite ``event.source.thread_id`` in place if the hook returns one.r  Nr1   ztopic recovery hook failedTexc_infor   )r(   ztopic recovery rewrite failed)	r   rz   r   rJ  r   r(   dataclassesr$  r1   )r  r8   recoverr1   	recovereds        r   _apply_topic_recoveryz)BasePlatformAdapter._apply_topic_recovery  s   $ 4d;;?F$//>F	II 	 	 	LL5LEEEFF	 I#f6F6L"2M2M M MF	I&.vYPPPELLL 	I 	I 	ILL84LHHHHHH	Is!   8 &A"!A"(B; ;&C%$C%c                     || _         dS )zESet an optional handler for messages arriving during active sessions.N)r  r   s     r   set_busy_session_handlerz,BasePlatformAdapter.set_busy_session_handler  s    %,"""r   session_storec                     || _         dS )a  
        Set the session store for checking active sessions.
        
        Used by adapters that need to check if a thread/conversation
        has an active session before processing messages (e.g., Slack
        thread replies without explicit mentions).
        N)_session_store)r  r@  s     r   set_session_storez%BasePlatformAdapter.set_session_store  s     ,r   c                 
   K   dS )z
        Connect to the platform and start receiving messages.
        
        Returns True if connection was successful.
        Nr  r   s    r   connectzBasePlatformAdapter.connect         	r   c                 
   K   dS )zDisconnect from the platform.Nr  r   s    r   
disconnectzBasePlatformAdapter.disconnect  s       	r   reply_toc                 
   K   dS )ar  
        Send a message to a chat.
        
        Args:
            chat_id: The chat/channel ID to send to
            content: Message content (may be markdown)
            reply_to: Optional message ID to reply to
            metadata: Additional platform-specific options
        
        Returns:
            SendResult with success status and message ID
        Nr  )r  r  rF  rI  r2   s        r   sendzBasePlatformAdapter.send  s      ( 	r   REQUIRES_EDIT_FINALIZEparent_chat_idr   c                 
   K   dS )u  Create a fresh thread under ``parent_chat_id`` for a session handoff.

        Used by the gateway's handoff watcher when transferring a CLI
        session to a thread-capable platform — the new thread isolates the
        handed-off conversation from any pre-existing chat in the home
        channel and gives users a clean per-handoff scrollback.

        Returns the new thread/topic id (as a string) on success, or
        ``None`` if the platform doesn't support threading or the
        attempt failed (permissions, topics-mode off, etc.). When ``None``
        is returned the watcher falls back to using ``parent_chat_id``
        directly.

        Default implementation returns ``None`` — adapters that support
        threads override this. See:
          - Telegram: forum topics in groups, DM topics with bot API 9.4+
          - Discord:  text-channel threads (1440-min auto-archive)
          - Slack:    seed-message thread anchoring
        Nr  )r  rM  r   s      r   create_handoff_threadz)BasePlatformAdapter.create_handoff_thread  s      0 tr   )finalizer/   rP  c                (   K   t          dd          S )uL  
        Edit a previously sent message. Optional — platforms that don't
        support editing return success=False and callers fall back to
        sending a new message.

        ``finalize`` signals that this is the last edit in a streaming
        sequence.  Most platforms (Telegram, Slack, Discord, Matrix,
        etc.) treat it as a no-op because their edit APIs have no notion
        of message lifecycle state — an edit is an edit.  Platforms that
        render streaming updates with a distinct "in progress" state and
        require explicit closure (e.g. rich card / AI assistant surfaces
        such as DingTalk AI Cards) use it to finalize the message and
        transition the UI out of the streaming indicator — those should
        also set ``REQUIRES_EDIT_FINALIZE = True`` so callers route a
        final edit through even when content is unchanged.  Callers
        should set ``finalize=True`` on the final edit of a streamed
        response (typically when ``got_done`` fires in the stream
        consumer) and leave it ``False`` on intermediate edits.
        FNot supportedr4  rb  ra  )r  r  r/   rF  rP  s        r   edit_messagez BasePlatformAdapter.edit_message
	  s      6 %????r   c                 
   K   dS )u  
        Delete a previously sent message.  Optional — platforms that don't
        support deletion return ``False`` and callers fall back to leaving
        the message in place.

        Used by the stream consumer's fresh-final cleanup path (see
        openclaw/openclaw#72038) to remove long-lived preview messages
        after sending the completed reply as a fresh message so the
        platform's visible timestamp reflects completion time.

        Returns ``True`` on successful deletion, ``False`` otherwise.
        Subclasses should override for platforms with a deletion API
        (e.g. Telegram ``deleteMessage``).
        Fr  )r  r  r/   s      r   delete_messagez"BasePlatformAdapter.delete_message'	  s      & ur   c                 r   	 ddl m} n# t          $ r Y dS w xY w	  |            }n# t          $ r Y dS w xY wt          |t                    r|                    di           ni }t          |t                    sdS |                    dd          }	 t          |          S # t          t          f$ r Y dS w xY w)a  Read ``display.ephemeral_system_ttl`` from config.

        Returns the TTL in seconds to use when an :class:`EphemeralReply`
        does not specify one explicitly.  ``0`` (the default) disables
        auto-deletion.  Non-fatal if config is unreadable.
        r   )load_configr  ephemeral_system_ttl)	hermes_cli.configrY  rz   r   r  r   r   r"   r#   )r  _load_configcfgr  r$   s        r   !_get_ephemeral_system_ttl_defaultz5BasePlatformAdapter._get_ephemeral_system_ttl_default<	  s    	EEEEEEE 	 	 	11		,..CC 	 	 	11	,6sD,A,AI#'')R(((r'4(( 	1kk0!44	s88O:& 	 	 	11	s*   	 

& 
44B! !B65B6ri  c                      d fd} |            }	 t          j        |           dS # t          $ r |                                 Y dS w xY w)u  Spawn a detached task that deletes ``message_id`` after ``ttl_seconds``.

        Best-effort — failures (gateway restart, permission denied, message
        too old for Telegram's 48h window) are swallowed at debug level.
        Does not block the caller.
        r   Nc                  F  K   	 t          j        t          dt                                         d {V                                 d {V  d S # t           j        $ r  t          $ r.} t                              dj	        |            Y d } ~ d S d } ~ ww xY w)NrJ   )r  r/   z*[%s] Ephemeral delete failed for %s/%s: %s)
rK  rL  r  r   rW  CancelledErrorrz   r   rJ  r   )r  r  r/   r  ri  s    r   _run_deletezCBasePlatformAdapter._schedule_ephemeral_delete.<locals>._run_deletea	  s      	mC3{+;+;$<$<=========))'j)QQQQQQQQQQQ)      @Iw
A        s   AA B 2#BB r   N)rK  create_taskr  close)r  r  r/   ri  rb  coros   ````  r   _schedule_ephemeral_deletez.BasePlatformAdapter._schedule_ephemeral_deleteT	  s    
	 
	 
	 
	 
	 
	 
	 
	 
	 {}}	%%%%% 	 	 	 JJLLLLLL		s   / AAr/  rw  
confirm_idc                 (   K   t          dd          S )u  Send a three-option slash-command confirmation prompt.

        Used by the gateway's generic slash-confirm primitive (see
        ``GatewayRunner._request_slash_confirm``) for commands that have a
        non-destructive but expensive side effect the user should explicitly
        acknowledge — the current caller is ``/reload-mcp``, which
        invalidates the provider prompt cache.

        Platforms with inline-button support (Telegram, Discord, Slack,
        Matrix, Feishu) should override this to render three buttons:
        Approve Once / Always Approve / Cancel.  Button callbacks MUST be
        routed back through the gateway by calling
        ``GatewayRunner._resolve_slash_confirm(confirm_id, choice)`` where
        ``choice`` is ``"once"`` / ``"always"`` / ``"cancel"``.

        Platforms without button UIs leave this as the default and fall
        through to the gateway's text fallback (which sends ``message`` as
        plain text and intercepts the next ``/approve`` / ``/always`` /
        ``/cancel`` reply).

        ``confirm_id`` is a short string generated by the gateway; the
        adapter stores it alongside any platform-specific state needed to
        route the callback (e.g. Telegram's ``_approval_state`` dict).
        FrR  rS  rT  )r  r  r/  r  rw  rh  r2   s          r   send_slash_confirmz&BasePlatformAdapter.send_slash_confirmv	  s      B %????r   questionchoices
clarify_idc                 d  K   |rd| dg}t          |d          D ] \  }}	|                    d| d|	            !|                    d           |                    d           d                    |          }
d	d
lm}  ||           nd| }
|                     ||
|           d{V S )u  Send a clarify prompt to the user.

        Two render modes:

          * **Multiple choice** (``choices`` is a non-empty list) — adapters
            that override this should render inline buttons (one per choice
            plus a final "Other" / free-text option).  Button callbacks
            MUST resolve via
            ``tools.clarify_gateway.resolve_gateway_clarify(clarify_id, response)``
            with the chosen string.  Picking the "Other" button calls
            ``mark_awaiting_text(clarify_id)`` so the next message in the
            session is captured as the response.

          * **Open-ended** (``choices`` is None or empty) — render the
            question as a plain text message; the next user message in the
            session is captured by the gateway's text-intercept and
            resolves the clarify automatically (see
            ``GatewayRunner._maybe_intercept_clarify_text``).

        The default implementation falls back to a numbered text list,
        which works on every platform — the user replies with a number
        ("2") or with the literal choice text, and the gateway intercepts
        and resolves.  For the text fallback path, the default calls
        ``mark_awaiting_text()`` so that the gateway text-intercept
        (:meth:`GatewayRunner._maybe_intercept_clarify_text`) catches the
        user's reply instead of timing out.
        Adapters with native button UIs (Telegram, Discord) SHOULD
        override this for a richer UX.
        u   ❓ r   rJ   )startz  z. z;Reply with the number, the option text, or your own answer.ry  r   )mark_awaiting_textr  rF  r2   N)	enumerater  jointools.clarify_gatewayrp  rK  )r  r  rk  rl  rm  rw  r2   linesichoicerm   rp  s               r   send_clarifyz BasePlatformAdapter.send_clarify	  s     L  	%&H&&+E&wa888 1 1	6/!//v//0000LLLLVWWW99U##D A@@@@@z****$($$DYY  
 
 
 
 
 
 
 
 	
r   user_idc                 D   K   |                      ||||           d{V S )zSend a notice privately when the platform supports it.

        The default implementation falls back to a normal send so callers can
        use one code path across platforms.
        r  rF  rI  r2   NrK  )r  r  ry  rF  rI  r2   s         r   send_private_noticez'BasePlatformAdapter.send_private_notice	  sM       YY	  
 
 
 
 
 
 
 
 	
r   c                 
   K   dS )z
        Send a typing indicator.
        
        Override in subclasses if the platform supports it.
        metadata: optional dict with platform-specific context (e.g. thread_id for Slack).
        Nr  )r  r  r2   s      r   send_typingzBasePlatformAdapter.send_typing	  rF  r   c                 
   K   dS )zStop a persistent typing indicator (if the platform uses one).

        Override in subclasses that start background typing loops.
        Default is a no-op for platforms with one-shot typing indicators.
        Nr  r  s     r   stop_typingzBasePlatformAdapter.stop_typing	  s       	r   r  r  human_delayc           	        K   ddl m} |D ]p\  }}|dk    rt          j        |           d{V  	 t                              d| j        t          |          |r
|dd         nd           |                    d          r5| 	                    | ||dd                   |r|nd|	           d{V }n\| 
                    |          r$|                     |||r|nd|
           d{V }n#|                     |||r|nd|           d{V }|j        s&t                              d| j        |j                   7# t          $ r.}	t                              d| j        |	d           Y d}	~	jd}	~	ww xY wdS )a  Send a batch of images.

        Accepts ``http(s)://``, ``file://`` URIs in the first tuple
        element.

        Default implementation sends each item individually,
        routing animated GIFs through ``send_animation`` and local
        files through ``send_image_file``.

        Override in subclasses to bundle into a single native API call
        (e.g. Signal's multi-attachment RPC)
        r   )unquoteNz[%s] Sending image: %s (alt=%s)   r   file://   )r  
image_pathcaptionr2   )r  animation_urlr  r2   )r  	image_urlr  r2   z[%s] Failed to send image: %sz[%s] Error sending image: %sTr8  )urllib.parser  rK  rL  r   infor   r  r   send_image_file_is_animation_urlsend_animation
send_imager4  rb  rz   )
r  r  r  r2   r  _unquoter  alt_text
img_resultimg_errs
             r   send_multiple_imagesz(BasePlatformAdapter.send_multiple_images	  sS     & 	544444#) "	` "	`IxQmK000000000`5I$Y//%-5HSbSMM2	   ''	22 '+';'; '#+8IabbM#:#:,4 >$!)	 (< ( ( " " " " " "JJ ++I66 '+':': '&/,4 >$!)	 (; ( ( " " " " " "JJ (, '"+,4 >$!)	 (7 ( ( " " " " " "J ") _LL!@$)ZM]^^^ ` ` `;TYZ^________`C"	` "	`s   DE
E:#E55E:r  r  c                 Z   K   |r| d| n|}|                      ||||           d{V S )z
        Send an image natively via the platform API.
        
        Override in subclasses to send images as proper attachments
        instead of plain-text URLs. Default falls back to sending the
        URL as a text message.
        ry  r{  Nr|  )r  r  r  r  rI  r2   rm   s          r   r  zBasePlatformAdapter.send_image0
  sR        -4B'((Y(((YYwxZbYcccccccccr   r  c                 F   K   |                      |||||           d{V S )z
        Send an animated GIF natively via the platform API.
        
        Override in subclasses to send GIFs as proper animations
        (e.g., Telegram send_animation) so they auto-play inline.
        Default falls back to send_image.
        )r  r  r  rI  r2   N)r  )r  r  r  r  rI  r2   s         r   r  z"BasePlatformAdapter.send_animationC
  sX       __WW^iq  }E_  F  F  F  F  F  F  F  F  	Fr   r   c                     |                                                      d          d         }|                    d          S )z=Check if a URL points to an animated GIF (vs a static image).r  r   r  )r   r   r   )r   r   s     r   r  z%BasePlatformAdapter._is_animation_urlT
  s6     		!!#&&q)~~f%%%r   c                 \  	 g }| }d}t          j        ||           D ]^}|                    d          }|                    d          	t          	fddD                       r|                    	|f           _d}t          j        ||           D ].}|                    d          	|                    	df           /|red |D             fd	}t          j        |||          }t          j        |||          }t          j        d
d|                                          }||fS )a  
        Extract image URLs from markdown and HTML image tags in a response.
        
        Finds patterns like:
        - ![alt text](https://example.com/image.png)
        - <img src="https://example.com/image.png">
        - <img src="https://example.com/image.png"></img>
        
        Args:
            content: The response text to scan.
        
        Returns:
            Tuple of (list of (url, alt_text) pairs, cleaned content with image tags removed).
        z$!\[([^\]]*)\]\((https?://[^\s\)]+)\)rJ   rD   c              3      K   | ]A}                                                     |          p|                                 v V  Bd S r   )r   r   )r   r:   r   s     r   r   z5BasePlatformAdapter.extract_images.<locals>.<genexpr>s
  sc       m ms399;;'',,Bsyy{{0B m m m m m mr   )r  r"  r  r  r  z	fal.mediazfal-cdnzreplicate.deliveryzA<img\s+src=["\']?(https?://[^\s"\'<>]+)["\']?\s*/?>\s*(?:</img>)?r   c                     h | ]\  }}|S r  r  )r   r   r   s      r   	<setcomp>z5BasePlatformAdapter.extract_images.<locals>.<setcomp>
  s    777fc1c777r   c                     | j         dk    r|                     d          n|                     d          }|v rdn|                     d          S )NrD   rJ   r   r   )	lastindexgroup)r]  r   extracted_urlss     r   _remove_if_extractedz@BasePlatformAdapter.extract_images.<locals>._remove_if_extracted
  sJ    (-1(<(<ekk!nnn%++a.. N22rrAFr   \n{3,}

)r   finditerr  r   r  r  r    )
rF  r  cleaned
md_patternr]  r  html_patternr  r  r   s
           @@r   extract_imagesz"BasePlatformAdapter.extract_imagesZ
  s      =
[W55 	/ 	/E{{1~~H++a..C m m m mkm m m m m /sHo... \[w77 	% 	%E++a..CMM3)$$$$  	A77777NG G G G G fZ)=wGGGf\+?IIGfY88>>@@Gwr   
audio_pathc                 `   K   d| }|r| d| }|                      ||||           d{V S )a
  
        Send an audio file as a native voice message via the platform API.
        
        Override in subclasses to send audio as voice bubbles (Telegram)
        or file attachments (Discord). Default falls back to sending the
        file path as text.
        u   🔊 Audio: ry  r{  Nr|  )r  r  r  r  rI  r2   r  rm   s           r   
send_voicezBasePlatformAdapter.send_voice
  s]        +j** 	(''''DYYwxZbYcccccccccr   rm   c                 b    t          j        dd|          dd                                         S )zPrepare text for TTS. Override to filter tool output, code, etc.

        Default strips markdown formatting and truncates to 4000 chars.
        z[*_`#\[\]()]r   Ni  )r   r  r    )r  rm   s     r   prepare_tts_textz$BasePlatformAdapter.prepare_tts_text
  s-    
 vor400$7==???r   c                 2   K    | j         d||d| d{V S )z
        Play auto-TTS audio for voice replies.

        Override in subclasses for invisible playback (e.g. Web UI).
        Default falls back to send_voice (shows audio player).
        )r  r  Nr  )r  )r  r  r  r  s       r   play_ttszBasePlatformAdapter.play_tts
  s9       %T_VWVVvVVVVVVVVVr   
video_pathc                 `   K   d| }|r| d| }|                      ||||           d{V S )z
        Send a video natively via the platform API.

        Override in subclasses to send videos as inline playable media.
        Default falls back to sending the file path as text.
        u   🎬 Video: ry  r{  Nr|  )r  r  r  r  rI  r2   r  rm   s           r   
send_videozBasePlatformAdapter.send_video
  s]       +j** 	(''''DYYwxZbYcccccccccr   	file_path	file_namec                 `   K   d| }|r| d| }|                      ||||           d{V S )z
        Send a document/file natively via the platform API.

        Override in subclasses to send files as downloadable attachments.
        Default falls back to sending the file path as text.
        u   📎 File: ry  r{  Nr|  )	r  r  r  r  r  rI  r2   r  rm   s	            r   send_documentz!BasePlatformAdapter.send_document
  s]        )Y(( 	(''''DYYwxZbYcccccccccr   r  c                 `   K   d| }|r| d| }|                      ||||           d{V S )a  
        Send a local image file natively via the platform API.

        Unlike send_image() which takes a URL, this takes a local file path.
        Override in subclasses for native photo attachments.
        Default falls back to sending the file path as text.
        u   🖼️ Image: ry  r{  Nr|  )r  r  r  r  rI  r2   r  rm   s           r   r  z#BasePlatformAdapter.send_image_file
  s]        .-- 	(''''DYYwxZbYcccccccccr   r  c                      t          |           S )zBReturn a resolved path if it is safe for native attachment upload.)r  r  s    r   r  z0BasePlatformAdapter.validate_media_delivery_path
  s     ,D111r   c                     g }| pg D ]r\  }}t          |          }t          |          }|r%|                    |t          |          f           Jt                              dt          |                     s|S )z5Drop unsafe MEDIA paths and normalize accepted paths.z(Skipping unsafe MEDIA directive path: %s)r   r  r  rS  r   r   r  )media_files
safe_media
media_pathr;   r$   	safe_paths         r   filter_media_delivery_pathsz/BasePlatformAdapter.filter_media_delivery_paths
  s     .0
$/$52 	` 	` Jj//C4S99I `!!9d8nn"=>>>>I>Z]K^K^____r   c                     g }| pg D ]`}t          |          }t          |          }|r|                    |           8t                              dt          |                     a|S )z?Drop unsafe bare local file paths and normalize accepted paths.z#Skipping unsafe local file path: %s)r   r  r  r   r   r  )
file_paths
safe_pathsr  r$   r  s        r   filter_local_delivery_pathsz/BasePlatformAdapter.filter_local_delivery_paths  s~     !#
#)r 	[ 	[Ii..C4S99I [!!),,,,DnUXFYFYZZZZr   c                 H   t          |           }t          |          }g }t          j        d| t          j                  D ]=}|                    |                                |                                f           >t          j        d|           D ]p}|                                }| t          d|dz
            |         }t          j	        d|          rG|                    ||                                f           qt          j        d| t          j
                  D ]=}|                    |                                |                                f           >|D ])\  }}t          ||          D ]}||         dk    rd||<   *d	                    |          S )
a_  Replace content inside fenced code blocks, inline code spans,
        and blockquotes with spaces to prevent MEDIA: false positives.

        Preserves character count so regex match offsets stay valid.
        Skips masking backtick-quoted paths in MEDIA: tags (e.g.
        ``MEDIA:`/path/to/file.png` ``) to avoid breaking path extraction.
        ```[^\n]*\n.*?```	`[^`\n]+`r      z
MEDIA:\s*$z^>.*$ry  r  r   )r   rE   r   r  DOTALLr  ro  endr  search	MULTILINErD  rs  )	rF  charsnspansr  ro  prefixr  rv  s	            r   _mask_protected_spansz)BasePlatformAdapter._mask_protected_spans  s    WJJ  17BIFF 	/ 	/ALL!''))QUUWW-.... \733 	+ 	+AGGIIESEBJ//56Fy// LL%)**** Xw== 	/ 	/ALL!''))QUUWW-....   	# 	#JE35#&& # #8t##"E!H# wwu~~r   c                 v   d| vsd| vr| S t          |           }t          j        d|           D ]v}|                    d          }t          j        d|          rJt          |                    d          |                    d                    D ]}||         dk    rd||<   wd                    |          S )	u  Blank out ``MEDIA:<bare-path>`` occurrences that sit inside a JSON
        string *value* so they are never delivered as real attachments.

        Serialized tool results frequently embed a previous reply's text, e.g.::

            {"result": "MEDIA:/Users/x/.hermes/media/generated/stale.png"}

        Here the ``MEDIA:`` is part of stored text, not an outbound directive,
        but the bare-path branch of ``MEDIA_TAG_CLEANUP_RE`` would still match it
        and re-deliver a stale file. (Regression report #34375.)

        The discriminator is precise so legitimate tags are untouched:

        * Only spans opened by a JSON value-context quote (``:``, ``,``, ``{`` or
          ``[`` immediately before the ``"``) are considered.
        * Within such a span, only a ``MEDIA:`` followed by a **bare** path
          (``/``, ``~/`` or ``X:\``) is masked. A ``MEDIA:"..."`` quoted-path
          tag — a real LLM output format the extractor supports — is not bare and
          is left alone.
        * Tags at line start, after prose whitespace, or indented are outside any
          JSON value span and are never affected.

        Offsets are preserved (matched chars replaced with spaces, newlines kept)
        so downstream match positions stay valid.
        r  zMEDIA:z$(?<=[:,{\[])\s*"((?:[^"\\\n]|\\.)*)"rJ   z MEDIA:\s*(?:~/|/|[A-Za-z]:[/\\])ry  r  r   )	r   r   r  r  r  rD  ro  r  rs  )rF  r  r  segrv  s        r   _mask_json_string_mediaz+BasePlatformAdapter._mask_json_string_media;  s    6 g!8!8NW DgNN 	' 	'A''!**Cy<cBB 'qwwqzz1558844 ' 'AQx4''#&awwu~~r   c                 t   g }| }d| v }|                     dd          }|                     dd          }t          }t                              |           }t                              |          }|                    |          D ]}|                    d                                          }t          |          dk    r8|d         |d         k    r&|d         dv r|d	d                                         }|	                    d          
                    d
          }|rS	 |                    t          j                            |          |f           # t          t           t"          f$ r Y w xY w|rt                              |          }t                              |          }d |                    |          D             }	|	rht%          |          }
t'          |	d          D ]
\  }}|
||= d                    |
          }t+          j        dd|                                          }||fS )u^  
        Extract MEDIA:<path> tags and [[audio_as_voice]] directives from response text.

        The TTS tool returns responses like:
            [[audio_as_voice]]
            MEDIA:/path/to/audio.ogg

        Skills that produce large/lossless images (e.g. info-graph, where a
        rendered JPG is 1-2 MB but Telegram's sendPhoto recompresses to
        ~200 KB at 1280px) can use ``[[as_document]]`` to request unmodified
        delivery via sendDocument instead of sendPhoto/sendMediaGroup. The
        directive is detected at the dispatch sites (which have access to the
        original response); this method just strips it so it never leaks into
        user-visible text. Per-file granularity is intentionally not exposed —
        when an agent emits ``[[as_document]]`` once, every image path in the
        same response is delivered as a document, mirroring the all-or-nothing
        scope of ``[[audio_as_voice]]``.

        Args:
            content: The response text to scan.

        Returns:
            Tuple of (list of (path, is_voice) pairs, cleaned content with tags removed).
        r  r   r  r  rD   r   r   r  rJ   r  c                 6    g | ]}|                                 S r  )span)r   r  s     r   r  z5BasePlatformAdapter.extract_media.<locals>.<listcomp>  s     NNN!QVVXXNNNr   T)r  r  r  )r$  r  rz  r  r  r  r  r    rE   r  r   r  r   r  r  r_   r  r#   r   sortedrs  r   r  )rF  mediar  has_voice_tagmedia_patternscan_contentr]  r  masked_cleanedr  r  ro  r  s                r   extract_mediaz!BasePlatformAdapter.extract_mediac  sF   4  -7//"6;; //"3R88 - +@@II*BB<PP"++L99 	 	E;;v&&,,..D4yyA~~$q'T"X"5"5$q'V:K:KAbDz''));;v&&--m<<D LL"'"4"4T":":M!JKKKKz:    H   		E0FFwOON0HHXXNNN}'='=n'M'MNNNE EW"("="="= ) )JE3eCi((''%..&FG<<BBDDg~s   $4EE32E3c                    t           }d                    d |D                       }t          j        d|z   dz   t          j                  }g t          j        d| t          j                  D ]=}                    |                                |	                                f           >t          j        d|           D ]=}                    |                                |	                                f           >dt          dt          ffd	}g }|                    |           D ]} ||                                          r |                    d
          }t          j                            |          }	t          j                            |	          r|                    ||	f           t"                              dt'          |                     t)                      }
g }|D ]5\  }}	|	|
vr,|
                    |	           |                    ||	f           6d |D             }| }|rF|D ]\  }}|                    |d          }t          j        dd|                                          }||fS )aD  
        Detect bare local file paths in response text for native delivery.

        Matches absolute paths (/...) and tilde paths (~/) ending in common
        image, video, audio, or document extensions.  Validates each
        candidate with ``os.path.isfile()`` to avoid false positives from
        URLs or non-existent paths.

        The extension list is broader than just images/video so the agent
        can produce arbitrary artifacts (charts, PDFs, spreadsheets, code
        archives, CSVs) and have them ship to the user as native uploads
        without needing an explicit ``MEDIA:`` tag.  Image / video
        extensions still embed inline where the platform supports it;
        document extensions route through ``send_document``.  The dispatch
        partition lives in ``gateway/run.py``.

        Paths inside fenced code blocks (``` ... ```) and inline code
        (`...`) are ignored so that code samples are never mutilated.

        Returns:
            Tuple of (list of expanded file paths, cleaned text with the
            raw path strings removed).
        r  c              3   @   K   | ]}|                     d           V  dS r  r  r  s     r   r   z:BasePlatformAdapter.extract_local_files.<locals>.<genexpr>  s,      EEaAHHSMMEEEEEEr   zB(?<![/:\w.])(?:~/|/|[A-Za-z]:[/\\])(?:[\w.\-]+[/\\])*[\w.\-]+\.(?:z)\br  r  posr   c                 <     t           fdD                       S )Nc              3   >   K   | ]\  }}|cxk    o|k     nc V  d S r   r  )r   rB   r  r  s      r   r   zLBasePlatformAdapter.extract_local_files.<locals>._in_code.<locals>.<genexpr>  s;      ;;1qC||||!||||;;;;;;r   )r   )r  
code_spanss   `r   _in_codez9BasePlatformAdapter.extract_local_files.<locals>._in_code  s'    ;;;;
;;;;;;r   r   z6Skipping bare file path in reply (no file on disk): %sc                     g | ]\  }}|S r  r  )r   r   r  s      r   r  z;BasePlatformAdapter.extract_local_files.<locals>.<listcomp>  s    444ka444r   r   r  r  )r  rs  r   compile
IGNORECASEr  r  r  ro  r  r   rS  r  r   r  r  isfiler   r  r  r  r  r$  r  r    )rF  _LOCAL_MEDIA_EXTSext_partpath_rer  r  foundr]  r$   r  r  uniquepathsr  _expr  s                  @r   extract_local_filesz'BasePlatformAdapter.extract_local_files  s   2 088EE3DEEEEE *QT\\_eeM
 
 
17BIFF 	4 	4Aqwwyy!%%''23333\733 	4 	4Aqwwyy!%%''23333	<# 	<$ 	< 	< 	< 	< 	< 	< %%g.. 	 	Ex&& ++a..Cw))#..Hw~~h'' c8_---- L"3''    EE" 	/ 	/MCt##"""sHo...44V444 	A# 3 3	T!//#r22fY88>>@@Gg~r          @interval
stop_eventc                 4  K   t          dt          d|dz
                      }	 	 |n|                                rZ	 t          | d          r-	 |                     |           d{V  n# t
          $ r Y nw xY w| j                            |           dS || j        vr	 t          j	        | 
                    ||          |           d{V  nW# t          j        $ r Y nFt          j        $ r  t
          $ r+}t                              d| j        |           Y d}~nd}~ww xY w|t          j        |           d{V  "t          j                    }|                                |z   }|                                sZ||                                z
  }	|	d	k    rn<t          j        t          d|	                     d{V  |                                Z|                                rZ	 t          | d          r-	 |                     |           d{V  n# t
          $ r Y nw xY w| j                            |           dS )# t          j        $ r Y nw xY w	 t          | d          r-	 |                     |           d{V  n# t
          $ r Y nw xY w| j                            |           dS # t          | d          r-	 |                     |           d{V  n# t
          $ r Y nw xY w| j                            |           w xY w)
u  
        Continuously send typing indicator until cancelled.
        
        Telegram/Discord typing status expires after ~5 seconds, so we refresh every 2
        to recover quickly after progress messages interrupt it.
        
        Skips send_typing when the chat is in ``_typing_paused`` (e.g. while
        the agent is waiting for dangerous-command approval).  This is critical
        for Slack's Assistant API where ``assistant_threads_setStatus`` disables
        the compose box — pausing lets the user type ``/approve`` or ``/deny``.

        Each ``send_typing`` call is bounded by a ~1.5s timeout so a slow
        network round-trip can't stall the refresh cadence.  Telegram- and
        Discord-side typing expire after ~5s; if any individual send_typing
        takes longer than the refresh interval, the bubble would die and
        stay dead until that call returns.  Abandoning the slow call lets
        the next tick fire a fresh send_typing on schedule — as long as
        one of them succeeds within the 5s platform-side window, the bubble
        stays visible across provider stalls / upstream API timeouts.
        g      ?r>  TNr  r#  rl   z&[%s] send_typing error (non-fatal): %sr   )r  minis_sethasattrr  rz   r  discardrK  wait_forr  TimeoutErrorra  r   rJ  r   rL  get_running_looprZ  )
r  r  r  r2   r  _send_typing_timeout
typing_errloopdeadline	remainings
             r   _keep_typingz BasePlatformAdapter._keep_typing  sM     <  #4S(T/)B)BCC1	1#)j.?.?.A.A)R t]++ **73333333333    D''00000[ $"555%. ,,Wx,HH$8           #/    "1   $   D Iz       
 %!-111111111/1199;;1$++-- > (499;; 6I A~~
 "-D)(<(<========= %++-- > $$&&  t]++ **73333333333    D''00000a#H % 	 	 	D	 t]++ **73333333333    D''00000 t]++ **73333333333    D''0000s   I A* *
A76A7	I  1C I D&!I #D&;!D!I !D&&CI H! !
H.-H.I I!J>  I!!J> 6J 
JJ>LK,+L,
K96L8K99Lc                 :    | j                             |           dS )u   Pause typing indicator for a chat (e.g. during approval waits).

        Thread-safe (CPython GIL) — can be called from the sync agent thread
        while ``_keep_typing`` runs on the async event loop.
        N)r  r  r  s     r   pause_typing_for_chatz)BasePlatformAdapter.pause_typing_for_chat^  s!     	(((((r   c                 :    | j                             |           dS )z;Resume typing indicator for a chat after approval resolves.N)r  r  r  s     r   resume_typing_for_chatz*BasePlatformAdapter.resume_typing_for_chatf  s    ##G,,,,,r   c                    K   |r0| j                             |          }||                                 	 |                     |           d{V  dS # t          $ r Y dS w xY w)zDSignal the active session loop to stop and clear typing immediately.N)r  r   r  r  rz   )r  rw  r  interrupt_events       r   interrupt_session_activityz.BasePlatformAdapter.interrupt_session_activityj  s       	&"377DDO*##%%%	""7+++++++++++ 	 	 	DD	s   A 
A! A!
generationcallbackr  c                  	 |rt          |          sdS | j                            |          }|t          |t                    rt          |          dk    r|\  }}nd|}}|$|"t          |          t          |          k     rdS t          |          r1|"| t          |          t          |          k    r|	|d	fd}|}||| j        |<   dS t          |          |f| j        |<   dS )u  Register a deferred callback to fire after the main response.

        ``generation`` lets callers tie the callback to a specific gateway run
        generation so stale runs cannot clear callbacks owned by a fresher run.

        If a callback for the same ``session_key`` (and generation, when set)
        is already registered, the new callback is chained — both fire, in
        registration order, with per-callback exception isolation. This lets
        independent features (background-review release + temporary-bubble
        cleanup) coexist without clobbering each other. Stale-generation
        callers never overwrite a fresher generation's slot.
        NrD   r   c                      	               n,# t           $ r t                              dd           Y nw xY w	                d S # t           $ r  t                              dd           Y d S w xY w)NzPost-delivery callback failedTr8  )rz   r   rJ  )_new_prevs   r   _chainedzEBasePlatformAdapter.register_post_delivery_callback.<locals>._chained  s    U$ U U U%DtTTTTTUU$ U U U%DtTTTTTTUs   
 &77
A &A10A1rc  )callabler  r   r   rf  rE   r   )
r  rw  r  r  r|  existing_genexisting_cbr  r  r  s
           @@r   register_post_delivery_callbackz3BasePlatformAdapter.register_post_delivery_callbacku  sJ   &  	(8"4"4 	F044[AA(E** ;s8}}/A/A,4)kk,0(k (*
OOc,&7&777 $$ $$%|$$J77#U U U U U U U $9AD)+666:=j//89TD)+666r   c                   |sdS | j                             |          }|dS t          |t                    rjt	          |          dk    rW|\  }}|"t          |          t          |          k    rdS | j                             |d           t          |          r|ndS |dS | j                             |d           t          |          r|ndS )zCPop a deferred callback, optionally requiring generation ownership.NrD   )r  r   r   rf  rE   r   popr  )r  rw  r  r   entry_generationr  s         r   pop_post_delivery_callbackz.BasePlatformAdapter.pop_post_delivery_callback  s      	4-11+>>=4eU## 	<E

a).&h%#.>*?*?3z??*R*Rt)--k4@@@'11;88t;!4%))+t<<< 1uuT1r   c                 
   K   dS )z.Hook called when background processing begins.Nr  )r  r8   s     r   on_processing_startz'BasePlatformAdapter.on_processing_start  
        r   outcomec                 
   K   dS )z1Hook called when background processing completes.Nr  )r  r8   r  s      r   on_processing_completez*BasePlatformAdapter.on_processing_complete  r  r   	hook_namerQ  r  c                    K   t          | |d          }t          |          sdS 	  ||i | d{V  dS # t          $ r-}t                              d| j        ||           Y d}~dS d}~ww xY w)zARun a lifecycle hook without letting failures break message flow.Nz[%s] %s hook failed: %s)r   r  rz   r   r   r   )r  r   rQ  r  hookr  s         r   _run_processing_hookz(BasePlatformAdapter._run_processing_hook  s      tY--~~ 	F	O$'''''''''''' 	O 	O 	ONN4diANNNNNNNNN	Os   6 
A- "A((A-rb  c                 t    | sdS |                                  t          fdt          D                       S )zGReturn True if the error string looks like a transient network failure.Fc              3       K   | ]}|v V  	d S r   r  )r   patlowereds     r   r   z:BasePlatformAdapter._is_retryable_error.<locals>.<genexpr>  s'      GGc3'>GGGGGGr   )r   r   _RETRYABLE_ERROR_PATTERNSrb  r'  s    @r   _is_retryable_errorz'BasePlatformAdapter._is_retryable_error  sC      	5++--GGGG-FGGGGGGr   c                 J    | sdS |                                  }d|v pd|v pd|v S )u   Return True if the error string indicates a read/write timeout.

        Timeout errors are NOT retryable and should NOT trigger plain-text
        fallback — the request may have already been delivered.
        Fz	timed outreadtimeoutwritetimeout)r   r)  s     r   _is_timeout_errorz%BasePlatformAdapter._is_timeout_error  s>      	5++--g%^')A^^W^E^^r   r  c                 4   t          |t                    r|j        }|5	 t          |                                           }n# t
          $ r d}Y nw xY w|r(|dk    r"t          |           j        t          j        u rd}|j	        t          |pd          fS |dfS )a  Unwrap a handler response into (text, ttl_seconds).

        Accepts a plain string, ``None``, or an :class:`EphemeralReply`.
        Returns ``(text, ttl)`` where ``ttl > 0`` means the caller should
        schedule a deletion via :meth:`_schedule_ephemeral_delete` after
        the send succeeds.  ``ttl`` is forced to 0 when the adapter
        doesn't override :meth:`delete_message` so non-supporting
        platforms silently degrade to normal sends.
        Nr   )
r   rh  ri  r   r^  rz   r  rW  rz  rm   )r  r  ttls      r   _unwrap_ephemeralz%BasePlatformAdapter._unwrap_ephemeral  s     h// 		0&C{dDDFFGGCC    CCC sQww4::#<@S@b#b#b=#chQ--//{s   !A AArD   max_retries
base_delayra  c           	        K   |                      ||||           d{V }|j        r|S |j        pd}|j        p|                     |          }	|	s|                     |          r|S |	rft          d|dz             D ]}
|d|
dz
  z  z  t          j        dd          z   }t          
                    d| j        |
|||           t          j        |           d{V  |                      ||||           d{V }|j        r%t                              d| j        |
           |c S |j        pd}|j        s|                     |          s nt                              d	| j        ||           d
}	 |                      ||||           d{V  n8# t          $ r+}t                              d| j        |           Y d}~nd}~ww xY w|S t          
                    d| j        |           |                      |d|dd          ||           d{V }|j        s&t                              d| j        |j                   |S )ax  
        Send a message with automatic retry for transient network errors.

        On permanent failures (e.g. formatting / permission errors) falls back
        to a plain-text version before giving up. If all attempts fail due to
        network errors, sends the user a brief delivery-failure notice so they
        know to retry rather than waiting indefinitely.
        r{  Nr   rJ   rD   r   z7[%s] Send failed (attempt %d/%d, retrying in %.1fs): %sz[%s] Send succeeded on retry %dz4[%s] Failed to deliver response after %d retries: %su   ⚠️ Message delivery failed after multiple attempts. Please try again — your request was processed but the response could not be sent.z/[%s] Could not send delivery-failure notice: %su3   [%s] Send failed: %s — trying plain-text fallbackz+(Response formatting failed, plain text:)

i  z"[%s] Fallback send also failed: %s)rK  r4  rb  rd  r*  r.  rD  randomuniformr   r   r   rK  rL  r  rz   rJ  )r  r  rF  rI  r2   r2  r3  r  	error_str
is_networkrP  delaynotice
notify_errfallback_results                  r   _send_with_retryz$BasePlatformAdapter._send_with_retry  s.     $ yy	 ! 
 
 
 
 
 
 
 
 > 	ML&B	%L)A)A))L)L
  	d44Y?? 	M  	 K!O44  "aGaK&89FN1a<P<PPMIwUI   mE*********#yy##%%	  )           > "KK A49gVVV!MMM"L.B	( D,D,DY,O,O E SUYU^`kmvwwwm k))GVhai)jjjjjjjjjj  k k kLL!RTXT]_ijjjjjjjjk 	LdiYbccc $		TGETENTT	 !* !
 !
 
 
 
 
 
 
 & 	aLL=ty/J_```s   8F 
G"!GGexisting_textnew_textc                     | s|S d |                      d          D             }|                                |vr|  d|                                 S | S )a`  Merge a new caption into existing text, avoiding duplicates.

        Uses line-by-line exact match (not substring) to prevent false positives
        where a shorter caption is silently dropped because it appears as a
        substring of a longer one (e.g. "Meeting" inside "Meeting agenda").
        Whitespace is normalised for comparison.
        c                 6    g | ]}|                                 S r  r   )r   cs     r   r  z6BasePlatformAdapter._merge_caption.<locals>.<listcomp>e  s     LLL1QWWYYLLLr   r  )r   r    )r>  r?  existing_captionss      r   r{  z"BasePlatformAdapter._merge_captionZ  so      	OLL0C0CF0K0KLLL>>#444#333399;;;r   c                 >    t          | dd           }|	i }|| _        |S )Nr  )r   r  )r  stores     r   _text_debounce_storez(BasePlatformAdapter._text_debounce_storej  s+    .55=E"'Dr   c           	         t          | dd          dk    oc|j        t          j        k    oNt          |dd           o<|                                 o't          |j        pd                                          }|rEt          	                    d| j
        t          |dd	          t          |j        pd                     |S )
z=Return True for normal text eligible for queue-mode debounce.r  r  queuerF  Fr   zC[%s] Queue-text debounce candidate accepted: session=%s text_len=%drw  r  )r   r<  r#  r)  rI  rS  rm   r    r   rJ  r   rE   )r  r8   r  s      r   !_is_queue_text_debounce_candidatez5BasePlatformAdapter._is_queue_text_debounce_candidateq  s     D+[99WD 1"k&661E:u5551 $$&&&1 ej&B--//00 	  	LLU	}c22EJ$"%%	   r   r|  c                     dt           dt          t          df         dz  fd} ||          } ||          }|duo||k    S )zDReturn True when two text debounce events came from the same sender.r   r   .Nc                 L   t          | dd           }|d S t          t          |dd                     }t          |dd           pt          |dd           }|r|t          |          fS t          |dd           dv r(t          |dd           r|dt          |j                  fS d S )	Nr1   r   user_id_altry  r*   >   r+   privater  r+   )r   r   r   r  )r   r1   r   senders       r   	_identityzFBasePlatformAdapter._can_merge_text_debounce_events.<locals>._identity  s    Y$77F~t%gfj$&G&GHHHV]D99]WVYX\=]=]F / #f++..v{D115FFF7SY[dfjKkKkF $FN(;(;<<4r   )r;  rf  r   )r  r|  r8   rO  existing_senderincoming_senders         r   _can_merge_text_debounce_eventsz3BasePlatformAdapter._can_merge_text_debounce_events  sf    
	 
	%S/D2H 
	 
	 
	 
	 $)H--#)E**d*Q//QQr   c                     |                                                      |          }|dS t          j                    }|j        | j        z   }|j        | j        z   }t          dt          ||          |z
            S )z<Return bounded busy-text debounce delay for ``session_key``.Nr  )
rF  r   rZ  	monotonicrY  r  rX  r  r  r  )r  rw  staterT  window_deadlinehard_cap_deadlines         r   _text_debounce_delayz(BasePlatformAdapter._text_debounce_delay  sw    ))++//<<=3n-$*JJ!NT-MM3O->??#EFFFr   c                 z  K   |                                  }|                    |          }||                     |j        |          s|                     |           d{V  |                    |          }|g|                     |j        |          sL| j                            |          }|.|                     ||          rt          | j        ||d           dS t          j                    }|t          |d||          }|||<   n|j
        r3|j        j
        r|j        j
         d|j
         n|j
        |j        _
        t          |dd          }|pt          |dd          }|t          |          |j        _        |.t          |j        d          rt          |          |j        _        ||_        |j        2|j                                        s|j                                         |                     |          }	t+          j        |                     ||	                    |_        dS )z@Buffer normal queue-mode busy text and schedule a bounded flush.NTrt  )r8   rW  rX  rY  ry  r/   r&   )rF  r   rR  r8   _flush_text_debounce_nowr  r  rZ  rT  rV  rm   r   r   r/   r  r&   rY  rW  donecancelrX  rK  rd  _flush_text_debounce)
r  rw  r8   rE  rU  existing_pendingrT  latest_message_idlatest_anchorr9  s
             r   _queue_text_debouncez(BasePlatformAdapter._queue_text_debounce  s^     ))++		+&&T%I%I%+W\%]%] //<<<<<<<<<IIk**E )M)Mek[`)a)a #'#9#=#=k#J#J #/D4X4XYikp4q4q//.##'	    n=%	  E "'E+z  {'$u{'775:777  
 !(|T B B-\@UW[1\1\M ,),->)?)?&(WU[BW-X-X(25m2D2D/EM:!%*//*;*;!J))+66()B)B;PU)V)VWW


r   r9  c                   K   	 t          j        |           d{V  |                     |           d{V  nf# t           j        $ rT Y t          j                    }|                                                     |          }||j        |u rd|_        dS dS dS w xY w	 t          j                    }|                                                     |          }||j        |u rd|_        dS dS dS # t          j                    }|                                                     |          }||j        |u rd|_        w xY w)z2Timer task that flushes the debounced text buffer.N)rK  rL  rZ  ra  current_taskrF  r   rW  )r  rw  r9  currentrU  s        r   r]  z(BasePlatformAdapter._flush_text_debounce  s     		"-&&&&&&&&&//<<<<<<<<<<% 	 	 	*,,G--//33K@@E UZ7%:%:!


 ! %:%:	 = *,,G--//33K@@E UZ7%:%:!


 ! %:%: *,,G--//33K@@E UZ7%:%:!
!!!!s'   5: C3 B	C3 BC3 3AEc                   K   |                                  }|                    |          }|dS t          j                    }|j        ;|j        |ur2|j                                        s|j                                         d|_        | j                            |          }||                     ||j	                  sdS |
                    |d          }|dS t          | j        ||j	        d           dS )z@Force-flush one debounced busy-text burst into the pending slot.NFTrt  )rF  r   rK  rc  rW  r[  r\  r  rR  r8   r  r  )r  rw  rE  rU  rd  r^  s         r   rZ  z,BasePlatformAdapter._flush_text_debounce_now  s     ))++		+&&=5&((:!ej&?&?
HYHY&?J
155kBB(889I5;WW ) 5		+t,,=5#"K		
 	
 	
 	
 tr   c                     |                                                      |d          }|;|j        6|j                                        s|j                                         dS dS dS dS )zACancel and drop pending text debounce state for control commands.N)rF  r  rW  r[  r\  )r  rw  rU  s      r   _discard_text_debouncez*BasePlatformAdapter._discard_text_debounce  so    ))++//TBB!7
@Q@Q!7J !7!7!7!7r   guardri  c                b    | j                             |          }|dS |||urdS | j         |= dS )ac  Release the adapter-level guard for a session.

        When ``guard`` is provided, only release the entry if it still points
        at that exact Event.  This lets reset-like commands swap in a temporary
        guard while the old processing task unwinds, without having the old
        task's cleanup accidentally clear the replacement guard.
        N)r  r   )r  rw  ri  current_guards       r   _release_session_guardz*BasePlatformAdapter._release_session_guard  sK     -11+>> Fe!;!;F!+...r   c                     | j                             |          }|dS t          |dd          }t          |o	 |                      S )uk  Return True if the owner task for ``session_key`` is done/cancelled.

        A lock is "stale" when the adapter still has ``_active_sessions[key]``
        AND a known owner task in ``_session_tasks`` that has already exited.
        When there is no owner task at all, that usually means the guard was
        installed by some path other than handle_message() (tests sometimes
        install guards directly) — don't treat that as stale.  The on-entry
        self-heal only needs to handle the production split-brain case where
        an owner task was recorded, then exited without clearing its guard.
        NFr[  )r  r   r   rS  )r  rw  rW  r[  s       r   _session_task_is_stalez*BasePlatformAdapter._session_task_is_stale!  sM     "&&{33<5tVT**DOTTVV$$$r   c                 X   || j         vrdS |                     |          sdS t                              d| j        |           | j                             |d           | j                            |d           | j                            |d           |                     |           dS )u  Clear a stale session lock if the owner task is already gone.

        Returns True if a stale lock was healed.  Returns False if there is
        no lock, or the owner task is still alive (the normal busy case).

        This is the on-entry safety net sidbin's issue #11016 analysis calls
        for: without it, a split-brain — adapter still thinks the session is
        active, but nothing is actually processing — traps the chat in
        infinite "Interrupting current task..." until the gateway is
        restarted.
        FzB[%s] Healing stale session lock for %s (owner task is done/absent)NT)	r  rn  r   r   r   r  r  r  rg  r  rw  s     r   _heal_stale_session_lockz,BasePlatformAdapter._heal_stale_session_lock2  s     d3335**;77 	5PI	
 	
 	

 	!!+t444"";555T222##K000tr   )r  r  c                   |pt          j                    }|| j        |<   t          j        |                     ||                    }|| j        |<   	 | j                            |           nC# t          $ r6 | j        	                    |d           | 
                    ||           Y dS w xY wt          |d          r>|                    | j        j                   |                    | j        j                   dS )aH  Spawn a background processing task under the given session guard.

        Returns True on success.  If the runtime stubs ``create_task`` with a
        non-Task sentinel (some tests do this), the guard is rolled back and
        False is returned so the caller isn't left holding a half-installed
        session lock.
        Nrh  Fadd_done_callbackT)rK  Eventr  rd  _process_message_backgroundr  r  r  r"   r  rl  r  rs  r  r  )r  r8   rw  r  ri  rW  s         r   _start_session_processingz-BasePlatformAdapter._start_session_processingM  s     27=??-2k*"4#C#CE;#W#WXX+/K(	"&&t,,,, 	 	 	 ##K666''5'AAA55	 4,-- 	K""4#9#ABBB""4#A#IJJJts   A. .<B.-B.Trelease_guarddiscard_pendingrx  ry  c                  K   | j                             |d          }||                                st                              d| j        |           | j                            |           |                                 	 t          j
        t          j        |          d           d{V  nt# t          j        $ r Y nct          j        $ r$ t                              d| j        |           Y n2t          $ r& t                              d| j        |d           Y nw xY w|r0| j                            |d           |                     |           |r|                     |           dS dS )	ux  Cancel in-flight processing for a single session.

        ``release_guard=False`` keeps the adapter-level session guard in place
        so reset-like commands can finish atomically before follow-up messages
        are allowed to start a fresh background task.

        Bounded by a 5s timeout so a wedged finally block in the cancelled
        task (typing-task cleanup, on_processing_complete hook, etc.) can't
        stall the calling dispatch coroutine — particularly under pytest-
        asyncio where the event loop's cancellation-propagation semantics
        differ subtly from a bare ``asyncio.run`` harness.
        Nz0[%s] Cancelling active processing for session %s      @r  zt[%s] Cancelled task for %s did not exit within 5s; unblocking dispatch and letting the task unwind in the backgroundz3[%s] Session cancellation raised while unwinding %sTr8  )r  r  r[  r   rJ  r   r  r  r\  rK  r  shieldra  r  r   rz   r  rg  rl  )r  rw  rx  ry  rW  s        r   cancel_session_processingz-BasePlatformAdapter.cancel_session_processingm  s     & "&&{D99DIIKKLLB	  
 *..t444KKMMM&w~d';';SIIIIIIIIIII)   '   XI{    
    II!	        	5"&&{D999''444 	5''44444	5 	5s   .B4 4D%/D%6,D%$D%command_guardc                    K   |                      |           d{V  | j                            |d          }|                     ||           |dS |                     ||           dS )u2  Resume the latest queued follow-up once a session command completes.

        Called at the tail of /stop, /new, and /reset dispatch.  Releases the
        command-scoped guard, then — if a follow-up message landed while the
        command was running — spawns a fresh processing task for it.
        Nrh  )rZ  r  r  rl  rv  )r  rw  r~  pending_events       r   $_drain_pending_after_session_commandz8BasePlatformAdapter._drain_pending_after_session_command  s       ++K888888888.22;EE##K}#EEE F&&}kBBBBBr   cmdc                   K   t                               d| j        ||           | j                            |          }t          j                    }|| j        |<   t          |j        t          |                    }	 | 
                    |           d{V }|                     |          \  }}	|rt                               d| j        |t          |          |j        j                   |                     |j        j        |t          |          |           d{V }
|	dk    r5|
j        r.|
j        r'|                     |j        j        |
j        |	           |                     |dd           d{V  nX# t(          $ rK | j                            |          |u r-|| j        v r||| j        |<   n|                     ||	            w xY w|                     ||           d{V  dS )
a  Dispatch a reset-like bypass command while preserving guard ordering.

        /stop, /new, and /reset must:
          1. Keep the session guard installed while the runner processes the
             command (so a racing follow-up message stays queued, not
             dispatched as a second parallel run).
          2. Cancel the old in-flight adapter task only AFTER the runner has
             finished handling the command (so the runner sees consistent
             state and its response is sent in order).
          3. Release the command-scoped guard and drain the latest queued
             follow-up exactly once, after 1 and 2 complete.
        8[%s] Command '/%s' bypassing active-session guard for %sNz4[%s] Sending command '/%s' response (%d chars) to %sr{  r   r  r/   ri  Frw  rh  )r   rJ  r   r  r   rK  rt  r5   r1   r9   r  r1  r  rE   r  r=  r4  r/   rg  r}  rz   r  rl  r  )r  r8   rw  r  rk  r~  thread_metar  _text_eph_ttl_rs              r    _dispatch_active_session_commandz4BasePlatformAdapter._dispatch_active_session_command  s     $ 	FI		
 	
 	
 -11+>>-:k*1%,@WX]@^@^__+	!22599999999H"44X>>OE8  JIJJL(    00!L0!4U;;(	 1         a<<BJ<2=<33 % 4#%=$, 4    00# % 1          
  	 	 	 $((55FF$"555-:S9FD)+66//=/QQQ	 77]SSSSSSSSSSSs   ?C>E> >AGc                 v
  K   | j         sdS t          |           |                     |           t          |j        | j        j                            dd          | j        j                            dd                    }|| j        v r| 	                    |           || j        v rx|
                                }ddlm}  ||          r|d	v rq|                     |           	 |                     |||           d{V  n;# t          $ r.}t                               d
| j        ||d           Y d}~nd}~ww xY wdS t                               d| j        ||           	 t)          |j        t+          |                    }|                      |           d{V }|                     |          \  }}	|rq|                     |j        j        |t+          |          |           d{V }
|	dk    r5|
j        r.|
j        r'|                     |j        j        |
j        |	           n;# t          $ r.}t                               d
| j        ||d           Y d}~nd}~ww xY wdS |s[	 ddlm} |                    |          du}n# t          $ r d}Y nw xY w|r't                               d| j        |           	 t)          |j        t+          |                    }|                      |           d{V }|                     |          \  }}	|rq|                     |j        j        |t+          |          |           d{V }
|	dk    r5|
j        r.|
j        r'|                     |j        j        |
j        |	           n:# t          $ r-}t                               d| j        |d           Y d}~nd}~ww xY wdS | j        Z	 |                     ||           d{V rdS n:# t          $ r-}t                               d| j        |d           Y d}~nd}~ww xY w|j         tB          j"        k    r9t                               d| j        |           tG          | j$        ||           dS | %                    |          rDt                               d| j        || j&                   | '                    ||           d{V  nLt                               d| j        |           tG          | j$        |||j         tB          j(        k               dS | )                    ||           dS )z
        Process an incoming message.
        
        This method returns quickly by spawning background tasks.
        This allows new messages to be processed even while an agent is running,
        enabling interruption support.
        Ngroup_sessions_per_userTthread_sessions_per_userF)r  r  r   )should_bypass_active_session>   newstopresetz&[%s] Command '/%s' dispatch failed: %sr8  r  r{  r  )clarify_gatewayz5[%s] Routing message to clarify text-intercept for %sz/[%s] Clarify text-intercept dispatch failed: %sz$[%s] Busy-session handler failed: %sz=[%s] Queuing photo follow-up for session %s without interruptun   [%s] New text message while session %s is active — debouncing follow-up (busy_text_mode=queue, window=%.2fs)uq   [%s] New message while session %s is active — queuing follow-up (no interrupt, will cascade after current turn)rt  )*r  r_  r=  r   r1   r  extrar   r  rq  rN  hermes_cli.commandsr  rg  r  rz   r   rb  r   rJ  r5   r9   r1  r=  r  r4  r/   rg  toolsr  get_pending_for_sessionr  r<  r#  r+  r  r  rI  r  ra  r)  rv  )r  r8   rw  r  r  r  _thread_metar  r  r  r  _clarify_mod_has_text_clarifys                r   handle_messagez"BasePlatformAdapter.handle_message  s      $ 	F(///
 	""5)))'L$(K$5$9$9:SUY$Z$Z%)[%6%:%:;UW\%]%]
 
 
 $///))+666 $/// ##%%CHHHHHH++C00 *
 222//<<<"CCE;X[\\\\\\\\\\$   D IsA %        
 F
 NIsK  m#>u|MdejMkMk#l#lL%)%:%:5%A%AAAAAAAH&*&<&<X&F&FOE8 #'#8#8$)L$8$)%<U%C%C%1	 $9 $ $       $a<<BJ<2=< ;;(-(<+-=,4 <   
 ! m m mLL!I49VY[\gkLllllllllm  &.EEEEEE$<<[IIQUU &% ! . . .(-%%%. % LLO	;  'B!L*A%*H*H( ( *.)>)>u)E)E#E#E#E#E#E#E*.*@*@*J*Jx  "'+'<'<(-(<(-)@)G)G)5	 (= ( ( " " " " " "B  (!||
|r}| $ ? ?,1L,@/1}08 !@ !" !" !"
 %   M Iq4 %        
 F)5f!77{KKKKKKKK   f f fLL!GTU`dLeeeeeeeef ![%666\^b^gituuu+D,BKQVWWW55e<< PI4   //UCCCCCCCCCCFI	   ,*$1[5EE	    F 	&&uk:::::st   +D	 	
E$D<<E)CH2 2
I*<$I%%I*3J J J CN 
O#OOO4 4
P+>#P&&P+c                     t          j        dd                                          } | dk    rdS | dk    r d\  }}t          j        |dz  |dz            S 	 t          t          j        dd                    }n# t          t          f$ r d	}Y nw xY w	 t          t          j        d
d                    }n# t          t          f$ r d}Y nw xY wt          j        |dz  |dz            S )ac  
        Return a random delay in seconds for human-like response pacing.

        Reads from env vars:
          HERMES_HUMAN_DELAY_MODE: "off" (default) | "natural" | "custom"
          HERMES_HUMAN_DELAY_MIN_MS: minimum delay in ms (default 800, custom mode)
          HERMES_HUMAN_DELAY_MAX_MS: maximum delay in ms (default 2500, custom mode)
        HERMES_HUMAN_DELAY_MODEr  r  natural)   	  g     @@HERMES_HUMAN_DELAY_MIN_MS800r  HERMES_HUMAN_DELAY_MAX_MS2500r  )r   getenvr   r5  r6  r   r"   r#   )r  min_msmax_mss      r   _get_human_delayz$BasePlatformAdapter._get_human_delay  s    y2E::@@BB5==39&NFF>&6/6F?CCC	#>FFGGFF:& 	 	 	FFF		#>GGHHFF:& 	 	 	FFF	~fvov???s$   "A: :BB"B7 7CCc           	      L,  ?@ABK   d@dA@Afd}| j                             |          pt          j                    }|| j         |<   t	          |j        t          |                    }d|i}	 t          j        | j	                  }n# t          t          f$ r d}Y nw xY w|	d|j        v r||d<   t          j         | j	        |j        j        fi |          Bd>Bfd}	 |                     d|           d{V  |                     |           d{V }	t#          |	t$                    }
|                     |	          \  }	}|	r@|                                r,|| j        v r#t,                              d	| j        |           d}	|	s+t,                              d
| j        |j        j                   |	rd|	v }|	}|                     |	          \  }}	|                     |          }|                     |	          \  }}t;          |                                          }|r<t,                              d| j        t?          |          t?          |	                     g }|
s]|                      |          \  }}| !                    |          }|r.t,                              d| j        t?          |                     |sd|sb|s`|s^t;          |	                                          }|r;t,          "                    d| j        t?          |          |j        j                   |}d}| #                    |j        j                  r|j$        tJ          j&        k    r|r|s	 ddl'm(}m)}  |            rpddl*}| +                    |          }|st          d          t          j,        ||           d{V }|-                    |          }|                    d          }n8# t\          $ r+}t,          "                    d| j        |           Y d}~nd}~ww xY wd}|rt_          |          0                                r	 d}| j1        td          j3        k    r|r|dd         |k    r|}| 4                    |j        j        |||           d{V }tk          |otm          |dd                    }	 to          j8        |           n:# tr          $ r Y n.w xY w# 	 to          j8        |           w # tr          $ r Y w w xY wxY w|r|st,                              d| j        t?          |          |j        j                   t          |          }|tu          |          }d|d<   nddi}| ;                    |j        j        |||           d{V } ||           |r;|dk    r5|j<        r.|j=        r'| >                    |j        j        |j=        |           | ?                                } |rt,                              d| j        t?          |                     	 | @                    |j        j        |||            d{V  n:# t\          $ r-}!t,          "                    d| j        |!d            Y d}!~!nd}!~!ww xY wh d!}"h d"}#dd#lAmB? g }$g }%|D ]`\  }&}'t_          |&          jC        D                                }(|(|#v r|'s|s|$E                    |&           I|%E                    |&|'f           ag })|D ]W}*t_          |*          jC        D                                |#v r|s|$E                    |*           B|)E                    |*           X|$rs	 ?fd$|$D             }+| @                    |j        j        |+||            d{V  n:# t\          $ r-}!t,          "                    d| j        |!d            Y d}!~!nd}!~!ww xY w|%D ]I\  }&}'| dk    rt          jF        |            d{V  	 t_          |&          jC        D                                },t          | j1        |,|'%          r)| H                    |j        j        |&|&           d{V }-nU|,|"v r)| I                    |j        j        |&|'           d{V }-n(| J                    |j        j        |&|(           d{V }-|-j<        s't,          "                    d)| j        |,|-jK                   # t\          $ r,}.t,          "                    d*| j        |.           Y d}.~.Cd}.~.ww xY w|)D ]}*| dk    rt          jF        |            d{V  	 t_          |*          jC        D                                },|,|"v r)| I                    |j        j        |*|'           d{V  n(| J                    |j        j        |*|(           d{V  # t\          $ r,}/t,          K                    d+| j        |*|/           Y d}/~/d}/~/ww xY w@p|p|p|p|}0|0sM|                                r9t,          K                    d,| j        t?          |          |j        j                   @rAntk          |	           }1|                     d-||1rt          jM        nt          jN                   d{V  | O                    |           d{V  || j        v r| j        P                    |          }2t,                              d.| j                   | j                             |          }3|3|3Q                                  |             d{V  t          j        | R                    |2|                    }4|4| jS        |<   	 | jT        U                    |4           |4V                    | jT        jW                   n# t          $ r Y nw xY w	  |             d{V  	 t          | d/          r%| Y                    |j        j                   d{V  n# t\          $ r Y nw xY wtm          |d0d          }5t          | d1          r| Z                    ||52          }6n%tm          | d3i           P                    |d          }6t          |6          r]	  |6            }7t          j\        |7          r!t          j]        |7t          4           d{V  n# t          j_        t\          f$ r Y nw xY w	 t          | d/          r%| Y                    |j        j                   d{V  n# t\          $ r Y nw xY w| O                    |           d{V  | j        P                    |d          }8|8t          j`                    }9| jS                            |          }:|:|:|9ur|8| j        |<   dS t,                              d5| j                   | j                             |          }3|3|3Q                                 t          j        | R                    |8|                    }4|4| jS        |<   	 | jT        U                    |4           |4V                    | jT        jW                   dS # t          $ r Y dS w xY wt          j`                    }9|9=| jS                            |          |9u r#| jS        |= | a                    ||6           dS dS dS nb# t          jb        $ rU t          j`                    }9t          jc        };|9	|9| jd        vrt          jN        };|                     d-||;           d{V   t\          $ r}<|                     d-|t          jN                   d{V  t,          K                    d7| j        |<d            	 t          |<          jf        }=t          |<          rt          |<          dd8         nd9}>t	          |j        t          |                    }| h                    |j        j        d:|= d;|> d<|=           d{V  n# t\          $ r Y nw xY wY d}<~<nd}<~<ww xY w |             d{V  	 t          | d/          r%| Y                    |j        j                   d{V  n# t\          $ r Y nw xY wtm          |d0d          }5t          | d1          r| Z                    ||52          }6n%tm          | d3i           P                    |d          }6t          |6          r]	  |6            }7t          j\        |7          r!t          j]        |7t          4           d{V  n# t          j_        t\          f$ r Y nw xY w	 t          | d/          r%| Y                    |j        j                   d{V  n# t\          $ r Y nw xY w| O                    |           d{V  | j        P                    |d          }8|8t          j`                    }9| jS                            |          }:|:|:|9ur|8| j        |<   dS t,                              d5| j                   | j                             |          }3|3|3Q                                 t          j        | R                    |8|                    }4|4| jS        |<   	 | jT        U                    |4           |4V                    | jT        jW                   dS # t          $ r Y dS w xY wt          j`                    }9|9=| jS                            |          |9u r#| jS        |= | a                    ||6           dS dS dS #  |             d{V  	 t          | d/          r%| Y                    |j        j                   d{V  n# t\          $ r Y nw xY wtm          |d0d          }5t          | d1          r| Z                    ||52          }6n%tm          | d3i           P                    |d          }6t          |6          r]	  |6            }7t          j\        |7          r!t          j]        |7t          4           d{V  n# t          j_        t\          f$ r Y nw xY w	 t          | d/          r%| Y                    |j        j                   d{V  n# t\          $ r Y nw xY w| O                    |           d{V  | j        P                    |d          }8|8t          j`                    }9| jS                            |          }:|:|:|9ur|8| j        |<   w t,                              d5| j                   | j                             |          }3|3|3Q                                 t          j        | R                    |8|                    }4|4| jS        |<   	 | jT        U                    |4           |4V                    | jT        jW                   w # t          $ r Y w w xY wt          j`                    }9|9<| jS                            |          |9u r!| jS        |= | a                    ||6           w w w xY w)?z4Background task that actually processes the message.Fc                 >    | d S dt          | dd          rdd S d S )NTr4  F)r   )r  delivery_attempteddelivery_succeededs    r   _record_deliveryzIBasePlatformAdapter._process_message_background.<locals>._record_delivery  s=    ~!%vy%00 *%)"""* *r   r2   Nr  r   c                     K                                      	 t          j        t          j                   d           d {V  d S # t          j        t          j        f$ r Y d S w xY w)Ng      ?r  )r\  rK  r  r|  ra  r  )typing_tasks   r   _stop_typing_taskzJBasePlatformAdapter._process_message_background.<locals>._stop_typing_task  s         &w~k'B'BCPPPPPPPPPPPP*G,@A    	s   .A	 	A('A(r  z:[%s] Suppressing stale response for interrupted session %sz0[%s] Handler returned empty/None response for %sr  z<[%s] extract_images found %d image(s) in response (%d chars)z5[%s] extract_local_files found %d file(s) in responsez[%s] response_delivery_recovered: extract pipeline reduced a non-empty response (%d chars) to empty with no attachment; delivering recovered original to %sr   )text_to_speech_toolcheck_tts_requirementsz!Empty text after markdown cleanupr  r  z[%s] Auto-TTS failed: %si   )r  r  r  r2   r4  z&[%s] Sending response (%d chars) to %sTnotifyr{  r  z1[%s] Extracted %d image(s) to send as attachments)r  r  r2   r  z[%s] Error batching images: %sr8  >   .3gprv  ru  rs  rr  rt  >   r  r"  r  r  r  )quotec                 0    g | ]}d  |           dfS )r  r   r  )r   r  _quotes     r   r  zCBasePlatformAdapter._process_message_background.<locals>.<listcomp>  s/    !T!T!T!#8VVAYY#8#8""=!T!T!Tr   )r;   )r  r  r2   )r  r  r2   )r  r  r2   z"[%s] Failed to send media (%s): %sz[%s] Error sending media: %sz$[%s] Error sending local file %s: %sz[%s] response_delivery_dropped: non-empty response (%d chars) produced no delivered message or attachment for %s (empty after extract, recovery yielded nothing).r  z([%s] Processing queued follow-up messager  _hermes_run_generationr  r
  r  r  uH   [%s] Late-arrival pending message during cleanup — spawning drain taskrh  z[%s] Error handling message: %si,  zno details availablezSorry, I encountered an error (z).
z2
Try again or use /reset to start a fresh session.rq  rc  )ir  r   rK  rt  r5   r1   r9   inspect	signaturer  r"   r#   
parametersrd  r  r#  r  r   rh  r1  r  r  r   r  r   rJ  r  r  r  r  r    rE   r  r  r   r  r<  r#  r.  tools.tts_toolr  r  r  r  	to_threadloadsrz   r   existsr   r   TELEGRAMr  rS  r   r   remover_   r  r=  r4  r/   rg  r  r  r  r  r   r   r  rL  rA   r  r  r  rb  r3  r7  r8  rZ  r  clearru  r  r  r  rs  r  r  r  r  r  isawaitabler  '_POST_DELIVERY_CALLBACK_TIMEOUT_SECONDSr  rc  rl  ra  r9  r  r  rB  r   rK  )Cr  r8   rw  r  r  _thread_metadata_keep_typing_kwargs_keep_typing_sigr  r  is_ephemeral_response_ephemeral_ttlforce_document_attachments_response_pre_extractr  r  text_contentlocal_files
_recovered	_tts_pathr  r  _jsonspeech_texttts_result_strtts_datatts_err_tts_caption_deliveredtelegram_tts_caption
tts_result_reply_anchorr  r  	batch_err_VIDEO_EXTS_IMAGE_EXTS_image_paths_non_image_mediar  r;   _ext_non_image_localr  _batchr:   media_result	media_errfile_err_anything_deliveredprocessing_okr  _active
drain_task_callback_generation_post_cb_post_resultlate_pendingrc  existing_taskr  r  
error_typeerror_detailr  r  r  r  sC                                                                  @@@@r   ru  z/BasePlatformAdapter._process_message_background  sj      #"	* 	* 	* 	* 	* 	* /33K@@SGMOO-<k* 7u|E\]bEcEcdd)+;<	$&01BCC:& 	$ 	$ 	$#	$#|7G7R'R'R0?-)D$ % 
 
	 	 	 	 	 	t	T++,A5IIIIIIIII "22599999999H$.x$H$H! (,'='=h'G'G$Hn 
 #**,,
   4#999PI  
   rOQUQZ\a\h\pqqq G .?(-J* )1% )-(:(:8(D(D%X">>{KK (,':':8'D'D$
  7|DDJJLL GKK ^`d`iknoukvkvx{  }E  yF  yF  G  G  G , z
 150H0H0V0V-K"&"B"B;"O"OK" z$[]a]fhklwhxhxyyy % 2 2+ 2 2 "9!B!B!H!H!J!JJ! 2Q !Is+@'A'A5<CW	   (2 !	225<3GHH W!.+2CCC( D + DW^^^^^^^^1133 	B0000*.*?*?*M*MK#. V&01T&U&U U3:3D 3+4 4 4 . . . . . .N (-{{>'B'BH(0[(A(AI$ W W W'A49gVVVVVVVVW */& !i!7!7!9!9 !!/3, MX->>> , ? ,UdU 3| C C3?0+/==$)L$8'0$8%5	 ,9 , , & & & & & &
 260ZWZTY5Z5Z2 2.!Ii0000& ! ! ! D!!Ii0000& ! ! ! D!   $(> $KK H$)UXYeUfUfhmhth|}}}$;E$B$BM (3+/0@+A+A(59(22,4d+;(#'#8#8 % 4 ,!.!1	 $9 $ $      F %$V,,, '
*Q.."N /"- / 77$)L$8'-'8(6 8    #3355  
nKK SUYU^`cdj`k`kllln"77$)L$8#)%5(3	 8           % n n n'GT]hlmmmmmmmmn
 POOHHH 988888%')+ ,7 H H(J
++288::D++$, ,$> , %++J7777(//X0FGGGG)+ !, ; ;IY.4466+EE$> F$++I6666(//	:::: 
n	n!T!T!T!T|!T!T!T"77$)L$8#)%5(3	 8           % n n n'GT]hlmmmmmmmmn -= ] ](J"Q%mK888888888]":..5;;==5dmSS[\\\ 15(-(<+5)9 2A 2 2 , , , , , ,LL
 !K//15(-(<+5)9 2A 2 2 , , , , , ,LL 261C1C(-(<*4)9 2D 2 2 , , , , , ,L  ,3 u"NN+OQUQZ\_amasttt$ ] ] ]'EtyR[\\\\\\\\] "2 m mI"Q%mK888888888m"9oo4::<<+--"&//(-(<+4)9 #2 # #         #'"4"4(-(<*3)9 #5 # #       
 % m m m%KTYXackllllllllm ' <*@ <<!,<0; $ + /D/J/J/L/L LLR 	3'<#=#=u|?S	   3E\..dS[nnJ\M++(-:Y!))@Q@Y         //<<<<<<<<< d444 $ 6 : :; G GGSSS /33K@@&MMOOO''))))))))) %044]KPP 

 4>#K0*..z:::001G1OPPPP    D @ $#%%%%%%%%%4// A**5<+?@@@@@@@@@    $+($ $ 
 t9:: `::3 ;  
 #4)CRHHLL[Z^__!! 	#+8::L*<88 %.($K           ,i8   D4// A**5<+?@@@@@@@@@    //<<<<<<<<<  155k4HHL'&355 $ 3 7 7 D D!-%\99 ;GD*;777LLb	   #377DDG*!(!488{SS" "J
 8BD'4.22:>>>"44T5K5STTTTT$   .  '355+0C0G0G0T0TXd0d0d+K8//?/SSSSS ,+0d0dm 5L % 	 	 	"/11L'1G#|4;Y'Y'Y+3++,DeWUUUUUUUUU 	 	 	++,DeM^MfgggggggggLL:DIqSWLXXX!!WW-
/21vvQs1vvdsd||;Q#>u|MdejMkMk#l#l ii!L0L* L L'L L L .                #	. $#%%%%%%%%%4// A**5<+?@@@@@@@@@    $+($ $ 
 t9:: `::3 ;  
 #4)CRHHLL[Z^__!! 	#+8::L*<88 %.($K           ,i8   D4// A**5<+?@@@@@@@@@    //<<<<<<<<<  155k4HHL'&355 $ 3 7 7 D D!-%\99 ;GD*;777LLb	   #377DDG*!(!488{SS" "J
 8BD'4.22:>>>"44T5K5STTTTT$   .  '355+0C0G0G0T0TXd0d0d+K8//?/SSSSS ,+0d0de $#%%%%%%%%%4// A**5<+?@@@@@@@@@    $+($ $ 
 t9:: `::3 ;  
 #4)CRHHLL[Z^__!! 	#+8::L*<88 %.($K           ,i8   D4// A**5<+?@@@@@@@@@    //<<<<<<<<<  155k4HHL'&355 $ 3 7 7 D D!-%\99 ;GD*;77LLb	   #377DDG*!(!488{SS" "J
 8BD'4.22:>>>"44T5K5STTTT$   .  '355+0C0G0G0T0TXd0d0d+K8//?/SSSS ,0dsB  /B	 	BB#Jz< %BO( 'z< (
P2!Pz< P(z< A2S 9S z< 
Sz< Sz< T S54T5
T?TTTDz< %)Y z< 
Z#Z<z< ZCz< 7^ z< 
_#_z< _+z< 8C)c#!z< #
d-!dz< d'z< A;f=<z< =
g3"g.)z< .g33E6z< *9n$ #z< $
n1.z< 0n11z< 5o< <
p	p	:?r: :ss5t 
tt9y 
y! y!:AL# <A,A@(A
A@3BA@@ A@@
A@@A@@A@@A@@AL# @A@@AL# @/5AA% A%
AA2A1AA2C#?AD# D#AD<D;AD<E 5AE6 E6
AFFAFJ9AJ< J<
AK
K	AK
L#AX#L55AM+M*AX#M+
AM8M5AX#M7AM8M8A0AX#O)?AP)P(AX#P)AQP?AX#QAQQAX#Q5AQ<Q;AX#Q<
AR	RAX#RAR	R	C<AX#V9AW V?AX#W 
AWW
AX#WAWWAAX#c           
        K   d}t          |          D ]}d | j        D             }|s n|D ]0}| j                            |           |                                 1	 t          j        t          j        d |D             ddid           d{V  # t
          j        $ r< t          
                    d	| j        t          d
 |D                                  Y  nw xY w| j                                         | j                                         | j                                         | j                                         | j                                         t#          |                                                                           D ];}|j        2|j                                        s|j                                         <|                                                                  dS )a  Cancel any in-flight background message-processing tasks.

        Used during gateway shutdown/replacement so active sessions from the old
        process do not keep running after adapters are being torn down.

        Each cancelled task is awaited with a 5s bound so a wedged finally
        (typing-task cleanup, on_processing_complete hook) can't stall the
        whole shutdown path.  Stragglers are released from our tracking and
        allowed to finish unwinding on their own.
           c                 :    g | ]}|                                 |S r  r[  )r   rW  s     r   r  z?BasePlatformAdapter.cancel_background_tasks.<locals>.<listcomp>  s%    PPPdDIIKKPTPPPr   c              3   >   K   | ]}t          j        |          V  d S r   )rK  r|  r   ts     r   r   z>BasePlatformAdapter.cancel_background_tasks.<locals>.<genexpr>  s,      ;;'.++;;;;;;r   return_exceptionsTr{  r  Nzo[%s] %d background task(s) did not exit within 5s; releasing tracking and letting them unwind in the backgroundc                 :    g | ]}|                                 |S r  r  r  s     r   r  z?BasePlatformAdapter.cancel_background_tasks.<locals>.<listcomp>&  s%    #E#E#E!AFFHH#EA#E#E#Er   )rD  r  r  r  r\  rK  r  gatherr  r   r   r   rE   r  r  r  r  r   rF  valuesrW  r[  )r  MAX_DRAIN_ROUNDSr   tasksrW  rU  s         r   cancel_background_tasksz+BasePlatformAdapter.cancel_background_tasks  s<     ( '(( 	 	APPd&<PPPE   .224888&N;;U;;;*.              '   SIs#E#Eu#E#E#EFF  
  	$$&&&&,,...!!###$$&&&##%%%$3355<<>>?? 	$ 	$Ez%ejoo.?.?%
!!###!!##))+++++s   5BAC C c                 R    || j         v o| j         |                                         S )z3Check if there's a pending interrupt for a session.)r  r  rp  s     r   has_pending_interruptz)BasePlatformAdapter.has_pending_interrupt5  s)    d33c8Mk8Z8a8a8c8ccr   c                 8    | j                             |d          S )z0Get and clear any pending message for a session.N)r  r  rp  s     r   get_pending_messagez'BasePlatformAdapter.get_pending_message9  s    %))+t<<<r   r+   	chat_name	user_namer(   
chat_topicrL  chat_id_altis_botguild_idrole_authorizedc                 h   ||                                 sd}t          | j        t          |          |||rt          |          nd||rt          |          nd|r|                                 nd||	|
|rt          |          nd|rt          |          nd|rt          |          nd|          S )z2Helper to build a SessionSource for this platform.N)r   r  r   r*   ry  r  r(   r  rL  r  r  r  rM  r/   r  )r    r   r   r   )r  r  r   r*   ry  r  r(   r  rL  r  r  r  rM  r/   r  s                  r   build_sourcez BasePlatformAdapter.build_source=  s    & !**:*:*<*<!J]LL$+5CLLL(1;c)nnnt-7Az'')))T##&.8S]]]D2@J3~...d*4>s:$+
 
 
 	
r   c                 
   K   dS )z
        Get information about a chat/channel.
        
        Returns dict with at least:
        - name: Chat name
        - type: "dm", "group", "channel"
        Nr  r  s     r   get_chat_infoz!BasePlatformAdapter.get_chat_infod  s       	r   c                     |S )z
        Format a message for this platform.
        
        Override in subclasses to handle platform-specific formatting
        (e.g., Telegram MarkdownV2, Discord markdown).
        
        Default implementation returns content as-is.
        r  )r  rF  s     r   format_messagez"BasePlatformAdapter.format_messageo  s	     r      
max_lengthrR   zCallable[[str], int]c                    |pt           } ||           |k    r| gS d}d}g }| }d}|r|d| dnd}	||z
   ||	          z
   ||          z
  }
|
dk     r|dz  }
 ||	           ||          z   ||z
  k    r|                    |	|z              n |t           urt          ||
|          }n|
}|d|         }|                    d          }||dz  k     r|                    d	          }|dk     r|}|d|         }|                    d
          |                    d          z
  }|dz  dk    r|                    d
          }|dk    r;||dz
           dk    r,|                    d
d|          }|dk    r||dz
           dk    ,|dk    rI|                    d	d|          }|                    dd|          }t          ||          }||dz  k    r|}|d|         }||d                                         }|	|z   }|du}|pd}|                    d          D ]n}|                                }|	                    d          rC|rd}d}2d}|dd                                         }|r|                                d         nd}o|r||z  }|}nd}|                    |           |t          |          dk    r*t          |          fdt          |          D             }|S )a9  
        Split a long message into chunks, preserving code block boundaries.

        When a split falls inside a triple-backtick code block, the fence is
        closed at the end of the current chunk and reopened (with the original
        language tag) at the start of the next chunk.  Multi-chunk responses
        receive indicators like ``(1/3)``.

        Args:
            content: The full message content
            max_length: Maximum length per chunk (platform-specific)
            len_fn: Optional length function for measuring string length.
                     Defaults to ``len`` (Unicode code-points).  Pass
                     ``utf16_len`` for platforms that measure message
                     length in UTF-16 code units (e.g. Telegram).

        Returns:
            List of message chunks
        
   z
```Nz```ry  r   rJ   rD   r  `z\`r   \r  FTrk   c                 2    g | ]\  }}| d |dz    d dS )z (rJ   r   r&  r  )r   rv  r  totals      r   r  z8BasePlatformAdapter.truncate_message.<locals>.<listcomp>  sG       19E5,,AE,,E,,,  r   )rE   r  rS   rfindr   r  r  r   r    r   rr  )rF  r  rR   _lenINDICATOR_RESERVEFENCE_CLOSEchunksr  
carry_langr  headroom	_cp_limitregionsplit_atr   backtick_countlast_bt
safe_splitnl_split
chunk_body
full_chunkin_codelangr   strippedtagr  s                             @r   truncate_messagez$BasePlatformAdapter.truncate_messagez  s   2 }4==J&&9	 %)
 S	& .8-C):))))F "$55VDttKGXGXXH!||%? tF||dd9oo->O1OOOfy0111 3.y(DII		$	z	z*F||D))H)q.((!<<,,!||$ ")8),I&__S11IOOE4J4JJN!Q&&#//#..kki!&<&D&D'ooc1g>>G kki!&<&D&DQ;;!*a!A!AJ(tQ@@H!$Z!:!:J!IN22#-"9H9-J!()),3355I*,J !,G#D"((.. 	= 	=::<<&&u-- = ="'!"&&qrrl002214<syy{{1~~" "k)
!

!
MM*%%%g  S	&l v;;??KKE   =Fv=N=N  F r   )NNr   rc  )Nr  )NNN)NNNN)r  NN)NNrD   r  )Nr+   NNNNNNFNNNF)r  N)rB  r  r  r  r  rS  r  r  r   r   r   r  rr  r   r   r  r  r   r   r   r  ra  r  r  r  r  r  r  r  r  r   r  r
  r  r  r	  r  r*  r-  r   r1  MessageHandlerr3  r6  r;  r=  r?  rC  r   rE  rH  rK  rL  rO  rU  rW  r^  rg  rj  r   rx  r}  r  r  r   r   r!   r  r  r  staticmethodr  r  r  r  r  r  r  r  r  r  r  r  r  r  r  rK  rt  r  r  r  r	  r  r  r  r3  r  r#  r*  r.  r1  r=  r{  r  rV  rF  rI  rR  rX  ra  r]  rZ  rg  rl  rn  rq  rv  r}  r  r  r  r  ru  r  r  r  r   r  r
  r  r)  r  r   r   rz  rz    sF         $ "'$&&& !$####E)~ E) E) E) E) E)N # 4    X D    X0 $(-1 C= 4S>* 
	   0 .2
 

 
 	

 4S>*
 

 
 
 
T/# /S /T / / / /* <A13(/ (/ (/s (/S (/+.(/8@(/ (/ (/ (/T 5 5 5 5 X5 )Xc] ) ) ) X) &(3- & & & X& +t + + + X+, , , , , ,,x9N8OQZ[_Q`cgQg8g/h ,mq , , , ,v v v v| | | |qS q3 qd qt q q q qr# rD r r r r@   C 3 s W[    (, , , , +c + + + X+ d    X(> (d ( ( ( (%XseXc]234% 
% % % %I< ID I I I I(-<QTBUW`aeWfBf9g0h -mq - - - -,s ,t , , , , t    ^    ^ 
 #'-1   3-	
 4S>* 
   ^6 $)D(((  
#	   B @ @ @@ @ 	@ @ 
@ @ @ @:  
	   *3    0     	 
 
       R .2!@ !@!@ !@ 	!@
 !@ !@ 4S>*!@ 
!@ !@ !@ !@T .27
 7
7
 7
 $	7

 7
 7
 4S>*7
 
7
 7
 7
 7
| #'-1
 

 #
 	

 3-
 4S>*
 

 
 
 
(            .2 7` 7`7` U38_%7` 4S>*	7`
 7` 
7` 7` 7` 7`z "&"&-1d dd d #	d
 3-d 4S>*d 
d d d d. "&"&-1F FF F #	F
 3-F 4S>*F 
F F F F" &s &t & & & \&
 - -d5c?.CS.H(I - - - \-f "&"&-1d dd d #	d
 3-d 4S>*d 
d d d d*@S @S @ @ @ @WW W
 
W W W W$ "&"&-1d dd d #	d
 3-d 4S>*d 
d d d d0 "&#'"&-1d dd d #	d
 C=d 3-d 4S>*d 
d d d d2 "&"&-1d dd d #	d
 3-d 4S>*d 
d d d d* 23 28C= 2 2 2 \2 
DsDy9I4J 
 
 
 \
 
49 
 
 
 \
 %s %s % % % \%P % % % % % \%N Qs QuT%T	2B-CS-H'I Q Q Q \Qf RS RU49c>-B R R R \Rn +/P1 P1P1 P1
 MD(P1 
P1 P1 P1 P1d)S )T ) ) ) )-c -d - - - -	C 	# 	RV 	 	 	 	  "&<U <U <U<U <U
 $J<U 
<U <U <U <UD "&	2 2 22 $J	2
 
D2 2 2 26=| = = = = =@, @IZ @_c @ @ @ @OC O Os OW[ O O O O H8C= HT H H H \H 	_# 	_4 	_ 	_ 	_ \	_# %s8J2K    4 #'P PP P 3-	P
 P P P 
P P P Pd hsm s s    \d30A+A&B    |     $R R\ R^b R R R R&G G G G G G2Xc 2X, 2XSW 2X 2X 2X 2Xh"c "% "D " " " "# $    < #  $        $ *.	/ / // &	/
 
/ / / /(%# %$ % % % %"C D    @ 48   
 "'-0 
   H # $15 15 1515 	15
 15 
15 15 15 15fCC }C 
	C C C C$KTKT KT 	KT
 
KT KT KT KTZ~;, ~;4 ~; ~; ~; ~;@ @e @ @ @ \@4bT| bTRU bTZ^ bT bT bT bTH5, 5, 5, 5,nd d d d d d=s =x7M = = = = $(!%#'#'$(%)%)"&(,$( %%
 %
%
 C=%
 	%

 #%
 C=%
 C=%
 SM%
 c]%
 c]%
 %
 3-%
 !%
 SM%
 %
  
!%
 %
 %
 %
N 3 4S>    ^	c 	c 	 	 	 	  37A AAA /0A 
c	A A A \A A Ar   rz  r   )F)r   )r"  )r"  rD   )rV  )r
   )r
   rD   )rr  )r8   r;  r   N)r  rK  r  rW   r@  r   r5  r   socketrZ   rw   rv   rZ  r)  abcr   r   r  r   utilsr   rA  rB  r   	frozensetr=   r?   r>   r  r   r   r!   r%   r  r5   r9   rS  rA   r   rG   rN   rS   rg   r   rf  r   r   r   r   r  r   r   r   r   r   r:  r   r   r   pathlibr   typingr   r   r   r   r   r   r   r   enumr   _Pathr  insert__file__r  r  gateway.configr   r   gateway.sessionr   r   hermes_constantsr   r   r   *GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGEr  r  r  r  bytesr!  r1  rS  rd  rh  ri  rm  ro  rx  r  ry  r|  r  SCREENSHOT_CACHE_DIRr  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r
  r  r  rs  r  rE   _MEDIA_EXT_ALTERNATIONr  r  r  r  r  r  r  r!  r#  r3  r;  rV  r[  Patternr_  ra  rh  r  r(  r*  r  r  r  rz  r  r   r   <module>r>     s/            				  				         



   # # # # # # # # ! ! ! ! ! ! % % % % % %		8	$	$
 iJJJKK #,)VV,<"="=  y&'!233 *. '$ $ $ $ $S 5 U     S4Z SWZ^S^    2.cDj . . . .. c T d    .+ + + + + +# c c    &# s s    &           F!C$J ! ! ! !H5C 5E#sTz/$: 5 5 5 5(49    &A &A3 &Ac &At &At &A &A &A &ARcDIoc3h&G#c(&RUY&Y ^b    . $( IM  Dj S	/E#s(O3c#h>E 	4Z	   D C$J  4        <#(d
 #(uT4Z7H #( #( #( #(L 3 d
 VZ    B     ( ( ( ( ( ( ( (             O O O O O O O O O O O O O O O O O O O O       ! ! ! ! ! ! 33uuX..008;<< = = = 3 3 3 3 3 3 3 3 < < < < < < < < U U U U U U U U U Ub +"& "&# "& "&S "& "& "& "&J  6 !.??T    E d    "  S c    88 8C 8c 8S 8QT 8 8 8 8v s C    8 !.>>T      S c    $8 8C 8c 8S 8QT 8 8 8 8D !.??   T      S c      $^$57GHH %~&9;PQQ   &&(( 9 "C *M ' ; = = = ##(( 7X%7W$7X%7[(7]*# 6 03 ,
# 
( $tDz    ? ? ? ? ?$-T - - - -d4j    "     B3 3u 3 3 3 3 3"$ d t    Ks Kx} K K K Kb BJABB 7 7 7 7 7 7

	? L J	
 L      L L  V P  X!" 
<#$ '   F " " F( U38_   . 
F77#6777S$OOO   "rz<>TUX&& M      E S S    @ # s    @ 
L 
L 
L 
L 
L 
L 
L 
L      . "&5B 5B 5B
5B 5B 	5B
 3-5B k5B 5B 5B 5Bp
 
 
 
 
$ 
 
 
        R R R R R R R Rj         BJFVVBJOQSQ^__BJ:BMJJD #U2:c?C+?%@      8 ) ) ) ) ) ) ) ).$! $! $! $! $!S $! $! $!X ;* ;* ;*3,-;*;* ;*
 ;* 
;* ;* ;* ;*J
   <.)HU3HXCX=Y4Z*[[\ !  Tz 	4Z	   B !4 444 Tz4 
#Y	4 4 4 4n.# .# . . . . {/ {/ {/ {/ {/# {/ {/ {/ {/ {/r   