
    o;i                    >   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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e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Z)d ee'e         z  e%ed!f         z  e*e         z  dz  defd"Z+	 ddd#d$edz  d ee'e         z  e%ed!f         z  e*e         z  dz  dedz  fd%Z,d&edz  de-fd'Z.d&edz  de%e-e-f         fd(Z/dd)ed*edz  defd+Z0dd,l1m2Z2m3Z3 dd-l4m4Z4 dd.l5m6Z6 dd/l7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z? dd0l@mAZA dd.l5m6ZB ejC        D                    d e eBeE          F                                jG        d1                              dd2lHmIZImJZJ dd3lKmLZLmMZM dd4lNmOZO d5ZPdd7ed8edefd9ZQd: ZR eOd;d<          ZSde6fd=ZTd>eUdefd?ZVdd>eUdedefdAZWdd7ededBedefdCZXddEedefdFZY eOdGdH          ZZde6fdIZ[dd>eUdedefdJZ\dd7ededBedefdKZ] eOdLdM          Z^dNdOdPdQdRdSZ_de6fdTZ`dd>eUdedefdVZa eOdWdX          Zbi dYdZd[d\d]d^d_d`dad^dbdcdddedfdgdhdgdidjdkd^dld^dmdndodpdqdrdsdtZcde6fduZdd>eUdvedefdwZeddEedefdxZf G dy dzeA          Zg G d{ d|eA          Zhe2 G d} d~                      Zi ejj        dejk                   ejj        dejk                   ejj        dejk                  fZle%ejm        e         d!f         end<   ddZoe2 G d d                      Zp G d de          Zqddde8eeif         dedeideddf
dZrdZse<eige=e:e?edf                           f         Zt	 dde-dededz  dedz  fdZu	 dde-dededz  de'e         dz  fdZv G d de          ZwdS )z
Base platform adapter interface.

All platform adapters (Telegram, Discord, WhatsApp) inherit from this
and implement the required methods.
    N)ABCabstractmethod)urlsplit)normalize_proxy_url>   .wav.flac.m4a.mp3.ogg.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{!!###    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   FtelegramT)r   _AUDIO_EXTSr   _TELEGRAM_VOICE_EXTS_TELEGRAM_AUDIO_ATTACHMENT_EXTS)r   r   r   normalized_exts       r   should_send_media_as_audior    +   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)r!   s    r   	utf16_lenr&   B   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      r#   N)r&   r$   )r!   r'   lohimids        r   _prefix_within_utf16_limitr-   Q   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   r)   r#   Nr$   )r!   r.   len_fnr*   r+   r,   s         r   _custom_unit_to_cpr2   d   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   r5   
ValueError_socketgetaddrinfo	AF_UNSPECSOCK_STREAMgaierrorOSError)r3   addrresolved_family_type_proto
_canonnamesockaddrs           r   is_network_accessiblerG   w   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HTTPPort1zhttp://:)
sysr   
subprocesscheck_outputDEVNULL	Exception
splitlinesstrip	partitionget)outpropslinekey_val
enable_keyhost_keyport_keyr3   ports              r   _detect_macos_system_proxyrj      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://.[]r)   rV   z[])r   r]   r   hostnamer   rstripri   
startswithr^   isdigitintcount
rpartition)r   rawparsedr3   rd   restri   
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   ,)osenvironr_   extendsplit)entriesrc   rw   s      r   _no_proxy_entriesr      s`    G' P PjnnS"%%OO		#OOOOOOONr   entryri   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*.r)   rm   )
r   r]   r   r{   r6   
ip_networkr7   r9   rr   endswith)	r   r3   ri   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   r3   ri   s     r   r   z&should_bypass_proxy.<locals>.<genexpr>  s0      OOe&udD99OOOOOOr   T)r   
isinstancer   listr{   any)r   r   
candidates	candidater3   ri   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   rj   )r   r   r   rc   detecteds        r   resolve_proxy_urlr     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   rr   aiohttp_socksr   from_urlImportErrorloggerwarningr   r   r   s      r   proxy_kwargs_for_botr   ;  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   rr   r   r   r   s      r   proxy_kwargs_for_aiohttpr   Y  s    (  2v(000000"++ID+AA	Y'++ ( ( (??''00 	NN1  
 r6MMMGY'''''(s   "+ AB=BBrp   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   r#   rm   r)   )	r   r   r_   r]   r   rer   rr   r   )rp   r   rw   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)Enumr#   )PlatformPlatformConfig)SessionSourcebuild_session_key)get_hermes_dirzSecure 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@r)   rl   /z/.../z/...rK   rm   z...)r   r   r[   schemenetlocrsplitpathr$   )	r   r   rw   rx   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   r9   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     !    $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      )s   GIF87as   GIF89ar#   s   BMs   RIFF   s   WEBPr0   )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   decoder9   r   uuiduuid4hexwrite_bytesr   )r   r   snippet	cache_dirfilenamefilepaths         r   cache_image_from_bytesr    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): N      >@Tr   rL   follow_redirectsevent_hooksr)   )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   r9   r   httpxlogging	getLogger__name__AsyncClientr   ranger_   raise_for_statusr  contentTimeoutExceptionHTTPStatusErrorr   r   status_codedebugasynciosleepr   r   r  r   r  _logclientattemptr   excwaits              r   cache_image_from_urlr3  *       $ -,,,,,;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                 H   ddl }t                      }|                                 | 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  r)   )timer   iterdiris_filestatst_mtimeunlinkr?   r7  r:  r  cutoffremovedfs         r   cleanup_image_cacherD  e  s     KKK#%%IYY[[MD01FG    99;; 	16688,v55


1   N   7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  r  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   r  Nr  Tr   r  r)   r  zaudio/*,*/*;q=0.8r  r  r  r  z*Audio cache retry %d/%d for %s (%.1fs): %s)r   r   r9   r   r  r   r!  r"  r#  r   r$  r_   r%  rM  r&  r'  r(  r   r   r)  r*  r+  r,  r-  s              r   cache_audio_from_urlrO    r4  r5  zcache/videosvideo_cachez	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_dirrX    r   r   rQ  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   )rX  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.pdfzapplication/pdfz.mdztext/markdownz.txtz
text/plainz.csvztext/csvz.logz.jsonzapplication/jsonz.xmlzapplication/xmlz.yamlzapplication/yamlz.ymlz.tomlzapplication/tomlz.iniz.cfgz.zipzapplication/zipz.docxzGapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentz.xlsxzAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetz.pptxzIapplication/vnd.openxmlformats-officedocument.presentationml.presentationc                  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   )rm   z..doc_Nr   rd   zPath traversal rejected: )r_  r   namer  r]   r  r	  r
  resolveis_relative_tor9   r  r   )r   r  r  	safe_namecached_namer  s         r   cache_document_from_bytesri    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                 H   ddl }t                      }|                                 | 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   Nr9  r)   )r:  r_  r;  r<  r=  r>  r?  r?   r@  s         r   cleanup_document_cacherk  ;  s     KKK&((IYY[[MD01FG    99;; 	16688,v55


1   NrE  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photovideoaudiovoicera  stickercommandN)r"  
__module____qualname____doc__TEXTLOCATIONPHOTOVIDEOAUDIOVOICEDOCUMENTSTICKERCOMMANDr   r   r   rm  rm  P  sA        %%DHEEEEHGGGGr   rm  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)r"  ru  rv  rw  SUCCESSFAILURE	CANCELLEDr   r   r   r  r  ]  s#        GGGGIIIr   r  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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_typeNsourceraw_message
message_idplatform_update_id)default_factory
media_urlsmedia_typesreply_to_message_idreply_to_text
auto_skillchannel_promptFinternal	timestampr   c                 6    | j                             d          S )z8Check if this is a command message (e.g., /new, /reset).r   )rM   rr   selfs    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.Nr)   maxsplitr   r   r   )r  rM   r   r   )r  partsrw   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.r)   r  r   u   ——z--u   —u   –-)r  rM   r   r$   r  )r  r  argss      r   get_command_argszMessageEvent.get_command_args  s       	9	++u::>>uQxxr||ND1199(DIIQQRZ\_``r   )"r"  ru  rv  rw  r   __annotations__rm  rx  r  r  r   r  r   r  r   r  rt   r   r   r  r   r  r  r  r  r  r  boolr   nowr  r  r  r  r   r   r   r  r  e  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((( Hd  %===Ix===)D ) ) ) )Xc]    #      r   r  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_PATTERNSeventc                 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   r  	chat_typedmz/restart)
r  rm  rx  rM   r]   rr   r   r  matchr[   )r  rM   r  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                   n    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S )	
SendResultzResult of sending a message.r  Nr  errorraw_responseF	retryable)r"  ru  rv  rw  r  r  r  r   r   r  r  r   r  r   r   r   r  r    si         &&MMM $J$$$E8C=L#Itr   r  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__r  )clsrM   r  instance	__class__s       r   r  zEphemeralReply.__new__  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.text  s     {{4   r   r   )r"  ru  rv  rw  r   rt   r  r   r  propertyrM   __classcell__)r  s   @r   r  r    s          ( # 3 Xc]      
 !c ! ! ! X! ! ! ! !r   r  )
merge_textpending_messagessession_keyr  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   rm  rz  r  r  r  r   r  rM   BasePlatformAdapter_merge_captionrx  )	r  r  r  r  existingexisting_is_photoincoming_is_photoexisting_has_mediaincoming_has_medias	            r   merge_pending_message_eventr  	  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  promptsrc   prompts         r   resolve_channel_promptr  a  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]   append)r  r  r  bindingsids_to_checkr   entry_idr  r!   seenrd  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   c                      e Zd ZU dZded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ddZdedededefdZddZedefd            Zedefd            ZdeddfdZde
eeegee         f                  ddfdZdeddfd Z e!defd!            Z"e!dd"            Z#e!	 	 dd
ed#ed$e
e         d%e
e$eef                  de%f
d&            Z&d'Z'ee(d(<   d'd)d
ed*ed#ed+ede%f
d,Z)d
ed*edefd-Z*de+fd.Z,d
ed*ed/e+ddfd0Z-	 dd
ed1eded2ed3ed%e
e$eef                  de%fd4Z.	 	 dd
ed5e
e         d#ed$e
e         d%e
e$eef                  de%fd6Z/dd
eddfd7Z0d
eddfd8Z1	 	 dd
ed:e2e3eef                  d%e
e$eef                  d;e4ddf
d<Z5	 	 	 dd
ed=ed>e
e         d$e
e         d%e
e$eef                  de%fd?Z6	 	 	 dd
ed@ed>e
e         d$e
e         d%e
e$eef                  de%fdAZ7e8dBedefdC            Z9e8d#ede3e2e3eef                  ef         fdD            Z:	 	 dd
edEed>e
e         d$e
e         de%f
dFZ;d
edEede%fdGZ<	 	 dd
edHed>e
e         d$e
e         de%f
dIZ=	 	 	 dd
edJed>e
e         dKe
e         d$e
e         de%fdLZ>	 	 dd
edMed>e
e         d$e
e         de%f
dNZ?e8d#ede3e2e3eef                  ef         fdO            Z@e8d#ede3e2e         ef         fdP            ZA	 	 	 dd
edRe4dSeBjC        dz  ddfdTZDd
eddfdUZEd
eddfdVZFd2ed
eddfdWZGddXd2edYedZe+dz  ddfd[ZHddXd2edZe+dz  dedz  fd\ZId]eddfd^ZJd]ed_eKddfd`ZLdaedbedceddfddZMe8dee
e         defdf            ZNe8dee
e         defdg            ZOdhede3e
e         e+f         fdiZP	 	 	 	 dd
ed#ed$e
e         d%edke+dle4ddmfdnZQe8doe
e         dpedefdq            ZRddrd2edse
eBjC                 ddfdtZSd2edefduZTd2edefdvZUddwd]ed2edxe
eBjC                 defdyZVdzdzd{d2ed|ed}eddfd~ZWd2edeBjC        ddfdZXd]ed2ededdfdZYd]eddfdZZe8de4fd            Z[d]ed2eddfdZ\ddZ]d2edefdZ^d2ede
e         fdZ_	 	 	 	 	 	 	 	 	 	 	 	 dd
ede
e         ded5e
e         de
e         de
e         de
e         de
e         de
e         dede
e         de
e         d*e
e         de`fdZae!d
ede$eef         fd            Zbd#edefdZce8	 	 dd#ede+de
d         de2e         fd            ZddS )r  z
    Base class for platform adapters.
    
    Subclasses implement platform-specific logic for:
    - Connecting and authenticating
    - Receiving messages
    - Sending messages/responses
    - Handling media
    configr   c                    || _         || _        d | _        d| _        d | _        d | _        d| _        d | _        i | _        i | _	        i | _
        t                      | _        i | _        t                      | _        d | _        d| _        t                      | _        t                      | _        t                      | _        d S )NFT)r  r   _message_handler_running_fatal_error_code_fatal_error_message_fatal_error_retryable_fatal_error_handler_active_sessions_pending_messages_session_tasksr  _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__  s     :>0437!&*#im! ;=:<79 58EE 9;%<?EE&_c" (-,/EE$-0UU% $'55r   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   chat_idc                 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   r  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   handlerNc                     || _         d S r   )r  r  r  s     r   set_fatal_error_handlerz+BasePlatformAdapter.set_fatal_error_handler  s    $+!!!r   c                     d| _         d | _        d | _        d| _        	 ddlm}  || j        j        dd d            d S # t          $ r Y d S w xY w)NTr   write_runtime_status	connectedr   platform_state
error_codeerror_message	r  r  r  r  gateway.statusr  r   r   r[   r  r  s     r   _mark_connectedz#BasePlatformAdapter._mark_connected  s    !%$(!&*#	;;;;;;  $-*=kfjz~ 	 	 	DD	   ? 
AAc                     d| _         | j        rd S 	 ddlm}  || j        j        dd d            d S # t          $ r Y d S w xY w)NFr   r  disconnectedr  )r  r  r  r  r   r   r[   r  s     r   _mark_disconnectedz&BasePlatformAdapter._mark_disconnected!  s     	F	;;;;;;  $-*=nim  ~B  C  C  C  C  C  C 	 	 	DD	s   3 
A Acodemessager  c                    d| _         || _        || _        || _        	 ddlm}  || j        j        d||           d S # t          $ r Y d S w xY w)NFr   r  fatalr  r  )r  r$  r%  r  r  s        r   _set_fatal_errorz$BasePlatformAdapter._set_fatal_error+  s    !%$+!&/#		;;;;;;  ,&%	       	 	 	DD	r   c                 r   K   | j         }|sd S  ||           }t          j        |          r
| d {V  d S d S r   )r  r+  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   metadataTpidNz already in usez (PID r  r   z. Stop the other gateway first.z[%s] %s_lockF)r  )r  r1  _platform_lock_scope_platform_lock_identityr   r   r   r  r_   r   r  rd  r(  )	r  r-  r.  r/  r1  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.r7  Nr   )release_scoped_lock)r   r  r<  r6  r7  )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   rd  zBasePlatformAdapter.name`  s     }"((***r   c                     | j         S )z(Check if adapter is currently connected.)r  r  s    r   is_connectedz BasePlatformAdapter.is_connectede  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_handlerj  s     !(r   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_handlers  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  rF  s     r   set_session_storez%BasePlatformAdapter.set_session_storew  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   r&  reply_tor3  c                 
   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  r&  rO  r3  s        r   sendzBasePlatformAdapter.send  s      ( 	r   FREQUIRES_EDIT_FINALIZE)finalizer  rS  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r  r  r  )r  r  r  r&  rS  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_configdisplayephemeral_system_ttl)	hermes_cli.configr\  r[   r   r  r_   rt   	TypeErrorr9   )r  _load_configcfgr]  rw   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r  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)Nr)   )r  r  z*[%s] Ephemeral delete failed for %s/%s: %s)
r+  r,  maxrt   rZ  CancelledErrorr[   r   r*  rd  )er  r  r  r  s    r   _run_deletezCBasePlatformAdapter._schedule_ephemeral_delete.<locals>._run_delete  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)r+  create_taskRuntimeErrorclose)r  r  r  r  ri  coros   ````  r   _schedule_ephemeral_deletez.BasePlatformAdapter._schedule_ephemeral_delete  s    
	 
	 
	 
	 
	 
	 
	 
	 
	 {}}	%%%%% 	 	 	 JJLLLLLL		s   / AAr?  r  
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).
        FrU  rV  rW  )r  r  r?  r%  r  rp  r3  s          r   send_slash_confirmz&BasePlatformAdapter.send_slash_confirm  s      B %????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  r&  rO  r3  NrQ  )r  r  rs  r&  rO  r3  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  r3  s      r   send_typingzBasePlatformAdapter.send_typingP  rL  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_typingY  s       	r           images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captionr3  )r  animation_urlr  r3  )r  	image_urlr  r3  z[%s] Failed to send image: %sz[%s] Error sending image: %sTexc_info)urllib.parser  r+  r,  r   inford  r   rr   send_image_file_is_animation_urlsend_animation
send_imager  r  r[   )
r  r  r}  r3  r~  _unquoter  alt_text
img_resultimg_errs
             r   send_multiple_imagesz(BasePlatformAdapter.send_multiple_imagesa  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                 X   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.
        r  r  r&  rO  Nrv  )r  r  r  r  rO  r3  rM   s          r   r  zBasePlatformAdapter.send_image  sO        -4B'((Y(((YYwxYPPPPPPPPPr   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  rO  r3  N)r  )r  r  r  r  rO  r3  s         r   r  z"BasePlatformAdapter.send_animation  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   .gif)r   r   r   )r   r   s     r   r  z%BasePlatformAdapter._is_animation_url  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\)]+)\)r)   r#   c              3      K   | ]A}                                                     |          p|                                 v V  Bd S r   )r   r   )r   r   r   s     r   r   z5BasePlatformAdapter.extract_images.<locals>.<genexpr>  sc       m ms399;;'',,Bsyy{{0B m m m m m mr   ).pngr  .jpegr  .webpz	fal.mediazfal-cdnzreplicate.deliveryzA<img\s+src=["\']?(https?://[^\s"\'<>]+)["\']?\s*/?>\s*(?:</img>)?r   c                     h | ]\  }}|S r   r   )r   r   rd   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 )Nr#   r)   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  subr]   )
r&  r}  cleaned
md_patternr  r  html_patternr  r  r   s
           @@r   extract_imagesz"BasePlatformAdapter.extract_images  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: r  r  Nrv  )r  r  r  r  rO  kwargsrM   s          r   
send_voicezBasePlatformAdapter.send_voice  sZ       +j** 	(''''DYYwxYPPPPPPPPPr   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: r  r  Nrv  )r  r  r  r  rO  r  rM   s          r   
send_videozBasePlatformAdapter.send_video  sZ       +j** 	(''''DYYwxYPPPPPPPPPr   	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: r  r  Nrv  )r  r  r  r  r  rO  r  rM   s           r   send_documentz!BasePlatformAdapter.send_document)  sZ       )Y(( 	(''''DYYwxYPPPPPPPPPr   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: r  r  Nrv  )r  r  r  r  rO  r  rM   s          r   r  z#BasePlatformAdapter.send_image_file=  sZ       .-- 	(''''DYYwxYPPPPPPPPPr   c                    g }| }d| v }|                     dd          }t          j        d          }|                    |           D ]}|                    d                                          }t          |          dk    r8|d         |d         k    r&|d         dv r|d	d                                         }|                    d                              d
          }|r4|	                    t          j                            |          |f           |r>|                    d|          }t          j        dd|                                          }||fS )a  
        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
        
        Args:
            content: The response text to scan.
        
        Returns:
            Tuple of (list of (path, is_voice) pairs, cleaned content with tags removed).
        [[audio_as_voice]]r   z[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|apk|ipa)(?=[\s`"',;:)\]}]|$)|\S+)[`"']?r   r#   r   r   z`"'r)   z
`"',.;:)}]r  r  )r  r   compiler  r  r]   r$   lstriprq   r  r   r   
expanduserr  )r&  mediar  has_voice_tagmedia_patternr  r   s          r   extract_mediaz!BasePlatformAdapter.extract_mediaQ  sf     -7//"6;; 
 B
 
 #++G44 	H 	HE;;v&&,,..D4yyA~~$q'T"X"5"5$q'V:K:KAbDz''));;v&&--m<<D Hbg0066FGGG  	A#''G44GfY88>>@@Gg~r   c                    d}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!                      }
g }|D ]5\  }}	|	|
vr,|
                    |	           |                    ||	f           6d |D             }| }|rF|D ]\  }}|                    |d          }t          j        dd|                                          }||fS )aW  
        Detect bare local file paths in response text for native media delivery.

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

        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  r  r  r  r  rQ  rR  rU  rT  rS  |c              3   @   K   | ]}|                     d           V  dS )rm   N)r  )r   rh  s     r   r   z:BasePlatformAdapter.extract_local_files.<locals>.<genexpr>  s,      EEaAHHSMMEEEEEEr   z/(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:z)\bz```[^\n]*\n.*?```z	`[^`\n]+`posr   c                 <     t           fdD                       S )Nc              3   >   K   | ]\  }}|cxk    o|k     nc V  d S r   r   )r   r!   rh  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   c                     g | ]\  }}|S r   r   )r   rd   expandeds      r   
<listcomp>z;BasePlatformAdapter.extract_local_files.<locals>.<listcomp>  s    444ka444r   r   r  r  )joinr   r  
IGNORECASEr  DOTALLr  startendrt   r  r  r   r   r  isfiler  r  r  r  r]   )r&  _LOCAL_MEDIA_EXTSext_partpath_remr  foundr  rw   r  r  uniquepathsr  _expr  s                  @r   extract_local_filesz'BasePlatformAdapter.extract_local_files{  st   "
 88EE3DEEEEE
 *>IFR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_--- 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{  r2  rL   z&[%s] send_typing error (non-fatal): %sr   )rf  minis_sethasattrr{  r[   r  discardr+  wait_forry  TimeoutErrorrg  r   r*  rd  r,  get_running_loopr:  )
r  r  r  r3  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_chat  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{  r[   )r  r  r  interrupt_events       r   interrupt_session_activityz.BasePlatformAdapter.interrupt_session_activity  s       	&"377DDO*##%%%	""7+++++++++++ 	 	 	DD	s   A 
A! A!
generationcallbackr  c                z    |rt          |          sdS ||| j        |<   dS t          |          |f| j        |<   dS )zRegister 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.
        N)callabler  rt   )r  r  r  r  s       r   register_post_delivery_callbackz3BasePlatformAdapter.register_post_delivery_callback(  sW      	(8"4"4 	F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.Nr#   )r  r_   r   tupler$   rt   popr  )r  r  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   r  c                 
   K   dS )z.Hook called when background processing begins.Nr   )r  r  s     r   on_processing_startz'BasePlatformAdapter.on_processing_startV  
        r   outcomec                 
   K   dS )z1Hook called when background processing completes.Nr   )r  r  r  s      r   on_processing_completez*BasePlatformAdapter.on_processing_completeY  r  r   	hook_namer  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  r[   r   r   rd  )r  r
  r  r  hookrh  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-r  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>l  s'      GGc3'>GGGGGGr   )r   r   _RETRYABLE_ERROR_PATTERNSr  r  s    @r   _is_retryable_errorz'BasePlatformAdapter._is_retryable_errorf  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_errorn  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   r  r  rt   rc  r[   typerZ  r  rM   )r  r   ttls      r   _unwrap_ephemeralz%BasePlatformAdapter._unwrap_ephemeralz  s     h// 		0&C{dDDFFGGCC    CCC sQww4::#<@S@b#b#b=#chQ--//{s   !A AAr#   max_retries
base_delayr  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.
        ru  Nr   r)   r#   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)rQ  r  r  r  r  r  r$  randomuniformr   r   rd  r+  r,  r  r[   r*  )r  r  r&  rO  r3  r  r  r+  	error_str
is_networkr0  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>  s     LLL1QWWYYLLLr   r  )r   r]   )r)  r*  existing_captionss      r   r  z"BasePlatformAdapter._merge_caption  so      	OLL0C0CF0K0KLLL>>#444#333399;;;r   guardr0  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  r  r0  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.
        NFdone)r  r_   r   r  )r  r  taskr5  s       r   _session_task_is_stalez*BasePlatformAdapter._session_task_is_stale	  sM     "&&{33<5tVT**DOTTVV$$$r   c                 .   || 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  r7  r   r   rd  r  r  r  r  r  s     r   _heal_stale_session_lockz,BasePlatformAdapter._heal_stale_session_lock	  s     d3335**;77 	5PI	
 	
 	

 	!!+t444"";555T222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.
        Nr/  Fadd_done_callbackT)r+  Eventr  rk  _process_message_backgroundr  r  r  r`  r  r3  r  r<  r  r  )r  r  r  r  r0  r6  s         r   _start_session_processingz-BasePlatformAdapter._start_session_processing9	  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_pendingrA  rB  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|r| 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 %sTr  )r  r  r5  r   r*  rd  r  r  cancelr+  r  shieldrg  r  r   r[   r  r3  )r  r  rA  rB  r6  s        r   cancel_session_processingz-BasePlatformAdapter.cancel_session_processingY	  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!	        	:"&&{D999 	5''44444	5 	5s   .B4 4D%/D%6,D%$D%command_guardc                    K   | 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.
        Nr/  )r  r  r3  r?  )r  r  rH  pending_events       r   $_drain_pending_after_session_commandz8BasePlatformAdapter._drain_pending_after_session_command	  s`       .22;EE##K}#EEE F&&}kBBBBBr   cmdc                   K   t                               d| j        ||           | j                            |          }t          j                    }|| j        |<   |j        j        rd|j        j        ind}	 | 	                    |           d{V }| 
                    |          \  }}	|rt                               d| j        |t          |          |j        j                   |                     |j        j        ||j        |           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 %s	thread_idNz4[%s] Sending command '/%s' response (%d chars) to %sru  r   r  r  r  Fr@  r/  )r   r*  rd  r  r_   r+  r=  r  rO  r  r  r  r$   r  r(  r  r  ro  rG  r[   r  r3  rK  )r  r  r  rL  r2  rH  thread_metar   _text_eph_ttl_rs              r    _dispatch_active_session_commandz4BasePlatformAdapter._dispatch_active_session_command	  s     $ 	FI		
 	
 	
 -11+>>-:k*?D|?U_{EL$:;;[_+	!22599999999H"44X>>OE8  JIJJL(    00!L0!"-(	 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   9C6E0 0AGc                   K   | j         sdS t          |           t          |j        | j        j                            dd          | j        j                            dd                    }|| j        v r|                     |           || j        v r|	                                }ddl
m}  ||          r{|d	v r\	 |                     |||           d{V  n;# t          $ r.}t                              d
| j        ||d           Y d}~nd}~ww xY wdS t                              d| j        ||           	 |j        j        rd|j        j        ind}|                      |           d{V }|                     |          \  }}	|ri|                     |j        j        ||j        |           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        t6          j        k    r9t                              d| j        |           t;          | j        ||           dS t                              d| j        |           || j        |<   | j        |                                          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)rW  rX  r   )should_bypass_active_session)stopnewresetz&[%s] Command '/%s' dispatch failed: %sr  rN  rO  ru  rP  z$[%s] Busy-session handler failed: %sz=[%s] Queuing photo follow-up for session %s without interruptuD   [%s] New message while session %s is active — triggering interrupt)!r  r  r   r  r  extrar_   r  r:  r  hermes_cli.commandsrY  rU  r[   r   r  rd  r*  rO  r  r(  r  r  r  ro  r  r  rm  rz  r  r  r  r?  )r  r  r  rL  rY  rh  _thread_metar   rR  rS  rT  s              r   handle_messagez"BasePlatformAdapter.handle_message	  sG      $ 	F(///'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LQLLb#lK1G#H#HhlL%)%:%:5%A%AAAAAAAH&*&<&<X&F&FOE8 #'#8#8$)L$8$)%*%5%1	 $9 $ $       $a<<BJ<2=< ;;(-(<+-=,4 <   
 ! m m mLL!I49VY[\gkLllllllllm)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 LL_aeajlwxxx27D";/!+.22444F 	&&uk:::::sI   C 
D)$DD?B:G: :
H2$H--H2?I 
J(#JJc                  4   t          j        dd                                          } | dk    rdS t          t          j        dd                    }t          t          j        dd                    }| dk    rd	\  }}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_MODEoffr|  HERMES_HUMAN_DELAY_MIN_MS800HERMES_HUMAN_DELAY_MAX_MS2500natural)i   i	  g     @@)r   getenvr   rt   r   r!  )modemin_msmax_mss      r   _get_human_delayz$BasePlatformAdapter._get_human_delay]
  s     y2E::@@BB5==3RY:EBBCCRY:FCCDD9&NFF~fvov???r   c           	      %  5678K   d6d767fd}| j                             |          pt          j                    }|| j         |<   |j        j        rd|j        j        ind}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 |          8d<8fd}	 |                     d	|           d{V  |                     |           d{V }	|                     |	          \  }	}
|	r@|                                r,|| j        v r#t&                              d
| j        |           d}	|	s+t&                              d| j        |j        j                   |	rv|                     |	          \  }}	|                     |	          \  }}|                    dd                                          }t7          j        dd|                                          }|r<t&                              d| j        t;          |          t;          |	                     |                     |          \  }}|r.t&                              d| j        t;          |                     d}|                     |j        j                  r|j         tB          j"        k    r|r|s	 ddl#m$}m%}  |            rddl&}t7          j        dd|          dd                                         }|st          d          t          j'        ||           d{V }|(                    |          }|                    d          }n8# tR          $ r+}t&          *                    d| j        |           Y d}~nd}~ww xY w|rtW          |          ,                                ry	 | -                    |j        j        ||           d{V  	 t]          j/        |           n:# t`          $ r Y n.w xY w# 	 t]          j/        |           w # t`          $ r Y w w xY wxY w|rt&                              d| j        t;          |          |j        j                   | 1                    |j        j        ||j2        |           d{V } ||           |
r;|
dk    r5|j3        r.|j2        r'| 4                    |j        j        |j2        |
           | 5                                }|rt&                              d| j        t;          |                     	 | 6                    |j        j        |||           d{V  n:# tR          $ r-}t&          *                    d| j        |d !           Y d}~nd}~ww xY wh d"}h d#}dd$l7m85 g }g }|D ]^\  }}tW          |          j9        :                                } | |v r|s|;                    |           G|;                    ||f           _g }!|D ]U}"tW          |"          j9        :                                |v r|;                    |"           @|!;                    |"           V|rs	 5fd%|D             }#| 6                    |j        j        |#||           d{V  n:# tR          $ r-}t&          *                    d| j        |d !           Y d}~nd}~ww xY w|D ]I\  }}|dk    rt          j<        |           d{V  	 tW          |          j9        :                                }$t{          | j>        |$|&          r)| ?                    |j        j        ||           d{V }%nU|$|v r)| @                    |j        j        ||'           d{V }%n(| A                    |j        j        ||(           d{V }%|%j3        s't&          *                    d)| j        |$|%jB                   # tR          $ r,}&t&          *                    d*| j        |&           Y d}&~&Cd}&~&ww xY w|!D ]}"|dk    rt          j<        |           d{V  	 tW          |"          j9        :                                }$|$|v r)| @                    |j        j        |"|'           d{V  n(| A                    |j        j        |"|(           d{V  # tR          $ r,}'t&          B                    d+| j        |"|'           Y d}'~'d}'~'ww xY w6r7nt          |	           }(|                     d,||(rt          jE        nt          jF                   d{V  || j        v rZ| j        G                    |          })t&                              d-| j                   | j                             |          }*|*|*H                                  |             d{V  t          j        | I                    |)|                    }+|+| jJ        |<   	 | jK        L                    |+           |+M                    | jK        jN                   n# t          $ r Y nw xY w	 t          |d.d          },t          | d/          r| Q                    ||,0          }-n%t          | d1i           G                    |d          }-t          |-          r	  |-             n# tR          $ r Y nw xY w |             d{V  	 t          | d2          r%| S                    |j        j                   d{V  n# tR          $ r Y nw xY w| j        G                    |d          }.|.t          jT                    }/| jJ                            |          }0|0|0|/ur|.| j        |<   dS t&                              d3| j                   | j                             |          }*|*|*H                                 t          j        | I                    |.|                    }+|+| jJ        |<   	 | jK        L                    |+           |+M                    | jK        jN                   dS # t          $ r Y dS w xY wt          jT                    }/|/=| jJ                            |          |/u r#| jJ        |= | U                    ||4           dS dS dS n\# t          jV        $ rU t          jT                    }/t          jW        }1|/	|/| jX        vrt          jF        }1|                     d,||1           d{V   tR          $ r}2|                     d,|t          jF                   d{V  t&          B                    d5| j        |2d !           	 t          |2          jZ        }3t          |2          rt          |2          dd6         nd7}4|j        j        rd|j        j        ind}| \                    |j        j        d8|3 d9|4 d:|;           d{V  n# tR          $ r Y nw xY wY d}2~2nd}2~2ww xY wt          |d.d          },t          | d/          r| Q                    ||,0          }-n%t          | d1i           G                    |d          }-t          |-          r	  |-             n# tR          $ r Y nw xY w |             d{V  	 t          | d2          r%| S                    |j        j                   d{V  n# tR          $ r Y nw xY w| j        G                    |d          }.|.t          jT                    }/| jJ                            |          }0|0|0|/ur|.| j        |<   dS t&                              d3| j                   | j                             |          }*|*|*H                                 t          j        | I                    |.|                    }+|+| jJ        |<   	 | jK        L                    |+           |+M                    | jK        jN                   dS # t          $ r Y dS w xY wt          jT                    }/|/=| jJ                            |          |/u r#| jJ        |= | U                    ||4           dS dS dS # t          |d.d          },t          | d/          r| Q                    ||,0          }-n%t          | d1i           G                    |d          }-t          |-          r	  |-             n# tR          $ r Y nw xY w |             d{V  	 t          | d2          r%| S                    |j        j                   d{V  n# tR          $ r Y nw xY w| j        G                    |d          }.|.t          jT                    }/| jJ                            |          }0|0|0|/ur|.| j        |<   w t&                              d3| j                   | j                             |          }*|*|*H                                 t          j        | I                    |.|                    }+|+| jJ        |<   	 | jK        L                    |+           |+M                    | jK        jN                   w # t          $ r Y w w xY wt          jT                    }/|/<| jJ                            |          |/u r!| jJ        |= | U                    ||4           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 )NTr  F)r   )r+  delivery_attempteddelivery_succeededs    r   _record_deliveryzIBasePlatformAdapter._process_message_background.<locals>._record_deliveryv
  s=    ~!%vy%00 *%)"""* *r   rO  Nr3  r  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  )rE  r+  r  rF  rg  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  r   zMEDIA:\s*\S+z<[%s] extract_images found %d image(s) in response (%d chars)z5[%s] extract_local_files found %d file(s) in responser   )text_to_speech_toolcheck_tts_requirementsz[*_`#\[\]()]i  z!Empty text after markdown cleanup)rM   r  z[%s] Auto-TTS failed: %s)r  r  r3  z&[%s] Sending response (%d chars) to %sru  rP  z1[%s] Extracted %d image(s) to send as attachments)r  r}  r3  r~  z[%s] Error batching images: %sTr  >   .3gprU  rT  rR  rQ  rS  >   r  r  r  r  r  )quotec                 0    g | ]}d  |           dfS )r  r   r   )r   p_quotes     r   r  zCBasePlatformAdapter._process_message_background.<locals>.<listcomp>=  s/    !T!T!T!#8VVAYY#8#8""=!T!T!Tr   )r   )r  r  r3  )r  r  r3  z"[%s] Failed to send media (%s): %sz[%s] Error sending media: %sz$[%s] Error sending local file %s: %sr	  z-[%s] Processing queued message from interrupt_hermes_run_generationr  r  r  r{  uH   [%s] Late-arrival pending message during cleanup — spawning drain taskr/  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.)r  r&  r3  rj  )]r  r_   r+  r=  r  rO  inspect	signaturer  r`  r9   
parametersrk  r  r  r  r  r  r  r   r  rd  r*  r  r  r  r]   r   r  r$   r  r  r  rm  r}  tools.tts_toolrv  rw  json	to_threadloadsr[   r   r   existsr  r   remover?   r(  r  r  ro  rm  r  r  ry  r   r   r  r,  r    r   r  r  r  r  r  r  r  r  r  clearr>  r  r  r  r<  r  r   r  r  r  r{  current_taskr3  rg  r  r  r  r"  r   rQ  )9r  r  r  rr  r  _thread_metadata_keep_typing_kwargs_keep_typing_sigru  r   _ephemeral_ttlmedia_filesr}  text_contentlocal_files	_tts_pathrv  rw  _jsonspeech_texttts_result_strtts_datatts_errr+  r~  	batch_err_VIDEO_EXTS_IMAGE_EXTS_image_paths_non_image_media
media_pathr   _ext_non_image_localr  _batchr   media_result	media_errfile_errprocessing_okrJ  _active
drain_task_callback_generation_post_cblate_pendingr  existing_taskr  rh  
error_typeerror_detailr|  rp  rq  rt  s9                                                        @@@@r   r>  z/BasePlatformAdapter._process_message_backgroundp
  s      #"	* 	* 	* 	* 	* 	* /33K@@SGMOO-<k* EJLDZdK)?@@`d)+;<	$&01BCC:& 	$ 	$ 	$#	$#|7G7R'R'R0?-)D$ % 
 
	 	 	 	 	 	M	T++,A5IIIIIIIII "22599999999H (,'='=h'G'G$Hn 
 #**,,
   4#999PI  
   rOQUQZ\a\h\pqqq um(,(:(:8(D(D%X (,':':8'D'D$+334H"MMSSUU!vor<HHNNPP GKK ^`d`iknoukvkvx{  }E  yF  yF  G  G  G -1,D,D\,R,R)\ vKK WY]Ybdghsdtdtuuu !	225<3GHH W!.+2CCC( D + DW^^^^^^^^1133 	B0000*,&"l*S*STYUYTY*Z*`*`*b*bK#. 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 !
!"mm$)L$8'0%5 ,         !Ii0000& ! ! ! D!!Ii0000& ! ! ! D!   KK H$)UXYeUfUfhmhth|}}}#'#8#8 % 4 ,!&!1!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{**8*$++J7777(//X0FGGGG)+ !, ; ;II-3355DD$++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 3E\..dS[nnJ\M++(-:Y!))@Q@Y         d444 $ 6 : :; G GLdiXXX /33K@@&MMOOO''))))))))) %044]KPP 

 4>#K0*..z:::001G1OPPPP    D P $+($ $ 
 t9:: `::3 ;  
 #4)CRHHLL[Z^__!! HJJJJ    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 ,+0d0dO 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LQLLb#lK1G#H#Hhl ii!L0L* L L'L L L .                #	> $+($ $ 
 t9:: `::3 ;  
 #4)CRHHLL[Z^__!! HJJJJ    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 ,+0d0dw $+($ $ 
 t9:: `::3 ;  
 #4)CRHHLL[Z^__!! HJJJJ    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 ,0ds  )B BBHs BM= <s =
N2!N-(s -N22&s (P' P s 
P$!s #P$$s 'Q)P>=Q>
QQ
QQC8s )U1 0s 1
V(;#V#s #V((Cs ;7Z3 2s 3
[*=#[% s %[**+s C)`?s 
`7!`2,s 2`77's A;cs 
d%"ds dDs 9i s 
is is 
k 
k%$k%95l/ /
l<;l<9q 
q('q(AB A,x/A
x:Bxx
xxxxAB xAB 
z 
z%$z%95{/ /
{<;{<9A@ @
A@(@'A@(BA.AKC0
AC;C:AKC;
ADDAKDADDAKD5AEEAKE
AEEAKEAEEC!AKI9AI;I:AKI;
AJJAKJAJJAAK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                                         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   r5  )r   r6  s     r   r  z?BasePlatformAdapter.cancel_background_tasks.<locals>.<listcomp>C  s%    PPPdDIIKKPTPPPr   c              3   >   K   | ]}t          j        |          V  d S r   )r+  rF  r   ts     r   r   z>BasePlatformAdapter.cancel_background_tasks.<locals>.<genexpr>L  s,      ;;'.++;;;;;;r   return_exceptionsTrD  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>U  s%    #E#E#E!AFFHH#EA#E#E#Er   )r$  r  r  r  rE  r+  r  gatherr  r   r   rd  r$   r  r  r  r  )r  MAX_DRAIN_ROUNDSrd   tasksr6  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  
  	$$&&&&,,...!!###$$&&&##%%%%%s   5BAC C c                 R    || j         v o| j         |                                         S )z3Check if there's a pending interrupt for a session.)r  r  r9  s     r   has_pending_interruptz)BasePlatformAdapter.has_pending_interrupt`  s)    d33c8Mk8Z8a8a8c8ccr   c                 8    | j                             |d          S )z0Get and clear any pending message for a session.N)r  r  r9  s     r   get_pending_messagez'BasePlatformAdapter.get_pending_messaged  s    %))+t<<<r   r  	chat_namer  	user_namerO  
chat_topicuser_id_altchat_id_altis_botguild_idparent_chat_idc                 f   ||                                 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  rs  r  rO  r  r  r  r  r  r  r  )r]   r   r   r   )r  r  r  r  rs  r  rO  r  r  r  r  r  r  r  s                 r   build_sourcez BasePlatformAdapter.build_sourceh  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_info  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  r&  s     r   format_messagez"BasePlatformAdapter.format_message  s	     r      
max_lengthr1   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```r  r   r)   r#    `z\`r   \r   FTrK   c                 2    g | ]\  }}| d |dz    d dS )z (r)   r   r  r   )r   ichunktotals      r   r  z8BasePlatformAdapter.truncate_message.<locals>.<listcomp>!  sG       19E5,,AE,,E,,,  r   )r$   r  r2   rfindru   rf  r  r   r]   rr   	enumerate)r&  r  r1   _lenINDICATOR_RESERVEFENCE_CLOSEchunksr  
carry_langprefixheadroom	_cp_limitregionsplit_atr   backtick_countlast_bt
safe_splitnl_split
chunk_body
full_chunkin_codelangrb   strippedtagr  s                             @r   truncate_messagez$BasePlatformAdapter.truncate_message  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   rj  )NNr   )Nr|  )NNN)r  NN)NNr#   r  )Nr  NNNNNNFNNN)r  N)er"  ru  rv  rw  r   r   r  r  r  r  r   r   r  r	  r  r  r   r   r  r  r#  r(  r,  r:  r=  rd  rA  MessageHandlerrC  r  rE  r   rI  r   rK  rN  r   r  rQ  rR  r  rX  rZ  rt   rc  ro  rr  rw  ry  r{  r   r   floatr  r  r  staticmethodr  r  r  r  r  r  r  r  r  r+  r=  r  r  r  r  r  r  r  r  r	  r  r  r  r  r(  r  r3  r7  r:  r?  rG  rK  rU  r`  rm  r>  r  r  r  r   r  r  r  r  r   r   r   r  r    s         1)~ 1) 1) 1) 1) 1)f 5 5 5 5 X5 )Xc] ) ) ) X) &(3- & & & X& +t + + + X+, , , , , ,,x9N8OQZ[_Q`cgQg8g/h ,mq , , , ,	 	 	 	   S 3 d t        C 3 s W[    (, , , , +c + + + X+ d    X(> (d ( ( ( (-<QTBUW`aeWfBf9g0h -mq - - - -,s ,t , , , , t    ^    ^ 
 #'-1   3-	
 4S>* 
   ^6 $)D((( @ @ @@ @ 	@ @ 
@ @ @ @:  
	   *3    0     	 
 
       R .2!@ !@!@ !@ 	!@
 !@ !@ 4S>*!@ 
!@ !@ !@ !@P #'-1
 

 #
 	

 3-
 4S>*
 

 
 
 
(            .2 7` 7`7` U38_%7` 4S>*	7`
 7` 
7` 7` 7` 7`z "&"&-1Q QQ Q #	Q
 3-Q 4S>*Q 
Q Q Q Q. "&"&-1F FF F #	F
 3-F 4S>*F 
F F F F" &s &t & & & \&
 - -d5c?.CS.H(I - - - \-f "&"&Q QQ Q #	Q
 3-Q 
Q Q Q Q(WW W
 
W W W W$ "&"&Q QQ Q #	Q
 3-Q 
Q Q Q Q. "&#'"&Q QQ Q #	Q
 C=Q 3-Q 
Q Q Q Q0 "&"&Q QQ Q #	Q
 3-Q 
Q Q Q Q( 's 'uT%T	2B-CS-H'I ' ' ' \'R AS AU49c>-B A A A \AL +/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. "&	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    \6 *.	/ / // &	/
 
/ / / /(%# %$ % % % %"C D    > 48   
 "'-0 
   H # $05 05 0505 	05
 05 
05 05 05 05dCC }C 
	C C C C"KTKT KT 	KT
 
KT KT KT KTZr;, r;4 r; r; r; r;h @e @ @ @ \@${T| {TRU {TZ^ {T {T {T {Tz1& 1& 1& 1&fd d d d d d=s =x7M = = = = $(!%#'#'$(%)%)"&(,$(#
 #
#
 C=#
 	#

 ##
 C=#
 C=#
 SM#
 c]#
 c]#
 #
 3-#
 !#
 SM#
 
#
 #
 #
 #
J 3 4S>    ^	c 	c 	 	 	 	  37A AAA /0A 
c	A A A \A A Ar   r  )Fr   )r   )r  )r  r#   )r6  )r   )r   r#   )rQ  )r  r  r   N)xrw  r+  r~  r6   r   r   r   r   socketr:   rX   rW   r  abcr   r   r  r   utilsr   r!  r"  r   	frozensetr   r   r   r   r   r  r    rt   r&   r-   r2   rG   rj   r   r{   r   r   r   r  r   r   r  r   r   r   dataclassesr   r   r   pathlibr   typingr   r   r   r   r   r   r   r   enumr   _Pathr   insert__file__re  r   gateway.configr   r   gateway.sessionr   r   hermes_constantsr   *GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGEr   r   r   r   bytesr   r  r3  rD  rH  rI  rM  rO  rW  SUPPORTED_VIDEO_TYPESrX  r[  r^  SUPPORTED_DOCUMENT_TYPESr_  ri  rk  rm  r  r  r  r  r  Patternr  r  r  r  r  r  r  r  r  r  r   r   r   <module>r     s^            				  				         



  # # # # # # # # ! ! ! ! ! ! % % % % % %		8	$	$
 iJJJKK #,)VV,<"="=  y&'!233 $ $ $ $ $ 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 < < < < < < < < + + + + + +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 
	? L J	
 L      L L  V P  X! (    E S S    @ # s    *
 
 
 
 
$ 
 
 
        L L L L L L L L` 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o! o! o! o! o!# o! o! o! o! o!r   