
    +j                        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mZm	Z	m
Z
 ddlmZmZ ddlmZ ddlmZmZmZmZmZmZmZmZ ddlmZ  ej        e          Z ej        d	ej                  Z  ej        d
ej!                  Z" ej        d          Z#dZ$e%e&df         e'd<   de&de&fdZ( G d d          Z)dS )u  
LightClaw outbound message sender — builds and sends messages to the server.
Mirrors: src/outbound.ts

Streaming mode: GatewayStreamConsumer is active (SUPPORTS_MESSAGE_EDITING=True).
Gateway calls send() with the first chunk, then edit_message() with accumulated
full text for subsequent chunks, and finally edit_message(finalize=True).

Round lifecycle:
  The round msgId spans the WHOLE agent turn (one user input → one round),
  so the front-end can aggregate streamed text, tool progress, post-stream
  follow-up text and attachment links into a single bubble.

  Closing the round (typing_stop + state cleanup) is the sole responsibility
  of stop_typing(), which the framework guarantees to invoke at turn end
  (see STREAMING_DESIGN.md §3.1 — at least four call sites).

  - Streaming:  inbound typing_start
                → send(first_chunk)
                → edit_message() × N
                → edit_message(finalize=True)        # only flushes residual delta
                → [optional tool_start / follow-up stream_chunk / attachment link]*
                → stop_typing() → typing_stop        # framework-driven close
  - Commands:   inbound typing_start
                → [tool_start]*
                → send(response)
                → stop_typing() → typing_stop        # framework-driven close
  - Standalone: typing_start → stream_chunk → typing_stop  (self-managed)
    N)AnyDictOptional)unquoteurlparse)
SendResult   )CHANNEL_KEYDEFAULT_AGENT_IDEVENT_MESSAGE_PRIVATE
KIND_USAGELOCALFILE_SCHEMEMEDIA_MAX_BYTESgenerate_msg_id
guess_mime)format_file_sizez+^.{1,2}\s+\w[\w_]*(?:\.\w+)*(?:\.\.\.|:|\()z)\bwrite_file\b.*?["\'/](/[^"\'<>\s,;}{]+)z"path"\s*:\s*"(/[^"]+)")u    ▉u    ▊u   ▉u   ▊._STREAM_CURSORStextreturnc                     | s| S t           D ]1}|                     |          r| dt          |                    c S 2| S )zRemove a trailing streaming-cursor glyph if present.

    Idempotent and safe to call multiple times.  Returns *text* unchanged
    when no cursor is found.
    N)r   endswithlen)r   cursors     </home/agentuser/.hermes/plugins/lightclawbot/src/outbound.py_strip_stream_cursorr   S   s[      ! ( (==   	(3v;;,''''	(K    c                      e Zd ZdZdededdfdZdededefdZdddde	dfd	ed
edede
e         de
e         de
e         dede
e         defdZdedefdZdeddfdZdedede
e         fdZdedede
e         fdZdededede
eeef                  ddf
dZ	 	 d7ded
ede
e         de
eeef                  dedefdZ	 d8dede
eeef                  deddfdZdededdfdZddd ded!ed
ed"ede
eeef                  dedefd#Z	 	 	 d9ded$ed%e
e         de
e         de
eeef                  defd&Zd'ede
e         fd(Z	 	 	 	 d:ded'ed%e
e         d)e
e         de
e         de
eeef                  defd*Zed'edefd+            Zed'ede
e         fd,            Zded'ede
e         fd-Zded
eddfd.Z deddfd/Z!	 	 d9ded'ed%e
e         d)e
e         de
e         defd0Z"	 	 d7ded1ed%e
e         de
e         def
d2Z#	 	 d7ded3ed%e
e         de
e         def
d4Z$	 	 d7ded5ed%e
e         de
e         def
d6Z%dS );OutboundMixina  
    Mixin providing the full public send API (streaming mode).

    Requires (set by LightClawAdapter):
        self._bot_client_id: str
        self._reliable: ReliableEmitter
        self._round_ids: dict[str, str]
        self._round_has_content: dict[str, bool]
        self._edit_snapshot: dict[str, str]
        self._incoming_agent_ids: dict[str, str]
        self._round_reply_to: dict[str, str]
        self._round_usage_emitted: dict[str, str]
        self._usage_tracker: SessionUsageTracker | None
    eventdatar   Nc                 T   | j         sdS |                    d          }|                    dd          }|                    dd          }|                    dd          }|r
|dd         nd}t                              d	||||           | j                             ||           dS )
u   Send frame via WebSocket — no ACK, no retry, no blocking.

        Delegates to ``ReliableEmitter.emit_fire_and_forget`` which injects
        ``idempotencyKey`` and calls the raw socket emit synchronously.
        NmsgIdkind?tocontent (   z@[lightclaw] fire_and_forget: kind=%s to=%s msgId=%s content='%s')	_reliablegetloggerinfoemit_fire_and_forget)selfr   r    msg_idr#   r%   r&   content_previews           r   _fire_and_forgetzOutboundMixin._fire_and_forgetu   s     ~ 	F'""xx$$XXdC  ((9b))*19'#2#,,rVX\^`bhjyzzz++E488888r   c                   K   | j         sdS |                    d          }|                    dd          }|                    dd          }t                              d||||           | j                             |||           d{V S )z/Send via ReliableEmitter with ACK + auto-retry.Fr"   r#   r$   r%   z:[lightclaw] emit_reliable: event=%s kind=%s to=%s msgId=%sN)r)   r*   r+   r,   emit_with_ack)r.   r   r    r/   r#   r%   s         r   _emit_reliablezOutboundMixin._emit_reliable   s      ~ 	5'""xx$$XXdC  PRWY]_acijjj^11%vFFFFFFFFFr   r   r%   r&   r#   reply_tofilesr/   agent_idextrac	                     |pt                      | j        ||t          t          j                    dz            ||d}
|r||
d<   |r||
d<   |||
d<   |	r|
                    |	           |
S )uV  Assemble a message:private frame payload.

        ``extra`` is carried verbatim under the top-level ``extra`` key
        (used by file:download signalling — see PROTOCOL.md).  Any other
        keyword arguments are merged into the top-level message dict for
        fields like ``toolName`` / ``toolPhase`` / ``idempotencyKey``.
          )r"   fromr%   r&   	timestampr#   agentIdreplyToMsgIdr6   Nr8   )r   _bot_client_idinttimeupdate)r.   r%   r&   r#   r5   r6   r/   r7   r8   passthroughmsgs              r   _build_messagezOutboundMixin._build_message   s    (  4?#4#4, TY[[4/00!
 
  	+"*C 	! CL CL 	$JJ{###
r   chat_idc                 Z    || j         vrt                      | j         |<   | j         |         S N)
_round_idsr   r.   rF   s     r   _get_or_create_round_idz%OutboundMixin._get_or_create_round_id   s.    $/))'6'8'8DOG$w''r   c                 r    | j                             |d            | j                            |d            d S rH   )rI   pop_round_has_contentrJ   s     r   _clear_round_idzOutboundMixin._clear_round_id   s9    GT***##GT22222r   c                    t          | dd          }|r|t          k    rdt           d| d| }ndt           d| }	 ddlm} n# t
          $ r Y dS w xY w ||          }|                    |          }|\|                                                                }|	                                D ]!\  }	}
|	                                |k    r|
} n"|sdS |                    d          p|                    d	          pdS )
u;  Look up the Hermes ``session_id`` for *(chat_id, agent_id)*.

        Mirrors the session_key shape used by inbound (and history's
        history-request handler).  Returns ``None`` when the entry is
        missing — typically the very first turn before the framework
        has flushed sessions.json.
        _sessions_dirNzagent:main:z:dm::r	   )load_session_store
session_id	sessionId)
getattrr   r
   historyrS   ImportErrorr*   striploweritems)r.   rF   r7   sessions_dirsession_keyrS   storeentryrZ   kvs              r   _resolve_session_id_for_chatz*OutboundMixin._resolve_session_id_for_chat   sT    '.dOT&J&J  	C$444MMMMM8MMKKBBBBBK	3333333 	 	 	44	 #"<00		+&&=%%''--//E  17799%%EE &  	4yy&&H%))K*@*@HDHs   A 
AAc                     t          | dd          }|sdS |                     ||          }|sdS t          j                            || d          S )zReturn absolute path of the usage sidecar jsonl for this chat.

        Returns ``None`` if we can't yet locate the session (best-effort).
        rQ   Nz.usage.jsonl)rV   rb   ospathjoin)r.   rF   r7   r\   rT   s        r   _resolve_usage_log_pathz%OutboundMixin._resolve_usage_log_path   sc     '.dOT&J&J 	466wII
 	4w||LZ*E*E*EFFFr   round_msg_idusagec           	         	 |                      ||          }|st                              d||           dS |t          t	          j                    dz            |d}t          j        |dd          }t          j        t          j	        
                    |          d	           t          |d
d          5 }|                    |dz              ddd           n# 1 swxY w Y   |t                              d||           dS t                              d|||                    d          |                    d                     dS # t          $ r(}	t                              d|||	           Y d}	~	dS d}	~	ww xY w)u  Append one usage line to ``<session_id>.usage.jsonl``.

        ``usage`` may be ``None`` — in that case we still write a line
        with ``"usage": null`` to preserve **ordinal alignment** with
        transcript turns: history-side join pairs the i-th sidecar entry
        with the i-th turn-end assistant, so a missing line would shift
        every subsequent turn by one and never self-heal.

        Best-effort: any error is logged at WARNING level and swallowed.
        The realtime usage frame has already been emitted (or skipped)
        by the caller, so a write failure only affects historical
        re-render — never the live UI.
        zK[lightclaw] usage persist skipped (session not yet indexed): to=%s msgId=%sNr:   )
roundMsgIdr<   ri   F),rR   )ensure_ascii
separatorsT)exist_okazutf-8)encoding
zD[lightclaw] usage placeholder persisted (no delta): path=%s msgId=%sz@[lightclaw] usage persisted: path=%s msgId=%s input=%s output=%sinputTokensoutputTokensz9[lightclaw] usage persist failed: chat=%s msgId=%s err=%s)rg   r+   r,   r@   rA   jsondumpsrd   makedirsre   dirnameopenwriter*   OSErrorwarning)
r.   rF   r7   rh   ri   log_pathr_   linefexcs
             r   _persist_turn_usagez!OutboundMixin._persist_turn_usage  s   (#	33GXFFH /\  
 *!$)++"455#% %E
 :e%JOOODK11DAAAAhg666 %!t$$$% % % % % % % % % % % % % % %}'l     )lIIm,,eii.G.G	      	 	 	NNKs        	sH   4E BE ;C E  C$$E 'C$(!E AE 
FE>>Fmetadatakwargsc           
        K   | j         st          ddd          S t          |          }| j                            |          pt
          }| j                            |          }|r
t          |o,t          |          dk     ot          
                    |                    }|rdnd}	|}
|	dk    r)| j                            |          rd|z   }
d| j        |<   t                              d	|	||t          |                     |                     t          |                     ||
|	||
                     |r|                     ||           |	dk    r
|| j        |<   t          d|          S | j                            |          }|rt          |o,t          |          dk     ot          
                    |                    }|rdnd}	|	dk    r|nd|z   }
t                              d|	||t          |                     |                     t          |                     ||
|	||
                     t          d|          S |                     |          }t          |o,t          |          dk     ot          
                    |                    }|rt                              d||t          |                     |                     t          |                     ||d||
                     |                     |           t          d|          S t                              d||t          |                     |                     t          |                     |dd||
                     |                     t          |                     ||d||
                     |                     t          |                     |dd||
                     |                     |           t          d|          S )u  Send a message to the user.

        Streaming mode behavior:
        - Round open (inbound already sent typing_start):
          - Tool progress → send as tool_start, keep round open
          - Stream first chunk → send as stream_chunk, record snapshot for
            subsequent edit_message() delta computation, keep round open
        - No round (standalone message, e.g. cron delivery):
          - typing_start → stream_chunk → typing_stop (full lifecycle)
        FzNot connectedT)successerror	retryablei  
tool_startstream_chunk

zB[lightclaw] send (%s, round open): to=%s msgId=%s content=%d charsr#   r/   r7   r   
message_idzH[lightclaw] send (%s, reuse last round): to=%s msgId=%s content=%d charszH[lightclaw] send (tool_start, no round): to=%s msgId=%s content=%d charsz>[lightclaw] send (standalone): to=%s msgId=%s content=%d charsr'   typing_starttyping_stop)r)   r   r   _incoming_agent_idsr*   r   rI   boolr   _TOOL_PROGRESS_REmatchrN   r+   r,   r1   r   rE   _track_write_file_path_edit_snapshot_last_round_idrK   rO   )r.   rF   r&   r5   r   r   r7   rh   is_tool_progressmsg_kindactual_contentlast_round_ids               r   sendzOutboundMixin.sendJ  s     $ ~ 	Te?dSSSS
 'w//+//88L<L **733 &	E#SCLL3.S3D3J3J73S3S    (8K||^H
 %N>))*..w77 6%+g%5N37'0KKT'<W   !!%##^('( $       >++GW=== >))/6#G, d|DDDD +//88 	F#SCLL3.S3D3J3J73S3S    (8K||^H
 $|33'9I  KKZ'=#g,,   !!%##^((8 $     d}EEEE 33G<<OGs*O/@/F/Fw/O/O
 
  	EKKZs7||   !!%##W<'( $       )))d|DDDD 	L\3w<<	
 	
 	
 	!.'3h   H H	
 	
 	

 	!~#h    	
 	
 	
 	!-'3h   H H	
 	
 	

 	W%%%$<@@@@r   c                 
   K   dS )u   Send typing indicator — no-op.

        Inbound already sends typing_start when a message arrives.
        LightClaw's typing_start persists until typing_stop — no refresh needed.
        N )r.   rF   r   r   s       r   send_typingzOutboundMixin.send_typing  s       	r   c                   K   | j                             |          }|sdS | j                            |          pt          }|                     |           d{V  | j        g| j                            |          |k    rH| j                            |          \  }}|t          	                    d|||                    d          |                    d          |                    d                     | 
                    t          |                     |dt          ||| j                            |          d|d                     |                     ||||	           nX|d
k    r6t          	                    d||           |                     |||d	           nt          	                    d||           || j        |<   t          	                    d||           | 
                    t          |                     |dd||                     || j        |<   | j                            |d           |                     |           dS )u  Stop typing indicator — sends typing_stop and closes the round.

        Called by the framework (gateway/run.py and base adapter) when:
        1. Agent finishes processing (success or error)
        2. Session processing completes in _process_message_background

        This is where the round gets properly closed with typing_stop.

        We also remember the closed round msgId in ``_last_round_id`` so
        that any output the framework routes AFTER this point (e.g.
        attachment links emitted via ``send_document`` / ``send_image``)
        can reuse the same msgId.  The reservation lives until the next
        inbound message clears it.
        NzB[lightclaw] emit usage: to=%s msgId=%s input=%s output=%s total=%srs   rt   totalTokensr'   )chatIdri   )r#   r/   r7   r5   r8   )rF   r7   rh   ri   unknownzA[lightclaw] usage placeholder (unmeasurable turn): to=%s msgId=%szI[lightclaw] usage skipped (no LLM turn, no sidecar entry): to=%s msgId=%sz'[lightclaw] stop_typing: to=%s msgId=%sr   r   )rI   r*   r   r   _deliver_pending_files_usage_tracker_round_usage_emittedclassify_turnr+   r,   r1   r   rE   r   _round_reply_tor   r   r   rM   rO   )r.   rF   r   rh   r7   ri   usage_reasons          r   stop_typingzOutboundMixin.stop_typing  s      **733 	F+//88L<L
 ))'222222222 +)--g66,FF"&"5"C"CG"L"LE< 2\IIm,,IIn--IIm,,   %%)''*+h!%!5!9!9'!B!B)+e<<	 (     ((#%!-	 )     ** %\  
 ((#%!-	 )     -\   2>D%g.=wUUU!-#h    	
 	
 	
 (4G$...W%%%%%r   F)finalizer   r   r   c          
      8  K   t          |          }| j                            |          }|r||k    rt          d|          S | j                            |d          }|                    |          r|t          |          d         }	n:|}	t                              d||t          |          t          |                     | j	                            |          pt          }
|	r=|                     t          |                     ||	d||
                     || j        |<   |r+t                              d||t          |	                     n7t                              d	||t          |	          |r
|d
d         nd           t          d|          S )u  Edit message — streaming delta computation and delivery.

        Called by GatewayStreamConsumer with accumulated full text (possibly
        with a cursor suffix like " ▉").  We strip the cursor, compute the
        delta against the previous snapshot, and emit a stream_chunk frame.

        ``finalize=True`` only marks the end of *one streaming segment*, NOT
        the end of the whole agent turn.  After a streamed answer the agent
        may still emit tool progress, follow-up text, attachment links, etc.,
        and they should all reuse the same round msgId so the front-end can
        aggregate them into a single bubble.

        Therefore round closure (typing_stop + ``_clear_round_id``) is
        delegated entirely to ``stop_typing()``, which the framework
        guarantees to invoke at the end of the turn (see STREAMING_DESIGN.md
        §3.1 — at least four guaranteed call sites).  Here we only flush the
        residual delta and keep the snapshot in sync so any subsequent
        ``edit_message`` calls within the same round still compute correct
        deltas.

        Tool progress edits (message_id != round_msg_id) are ignored.
        Tr   r'   Nz[[lightclaw] edit_message delta fallback: to=%s msgId=%s previous=%d chars, content=%d charsr   r   zi[lightclaw] edit_message finalize (round kept open for post-stream output): to=%s msgId=%s delta=%d charszJ[lightclaw] edit_message: to=%s msgId=%s delta=%d chars, content_tail='%s'i)r   rI   r*   r   r   
startswithr   r+   r|   r   r   r1   r   rE   r,   )r.   rF   r   r&   r   r   r   rh   previousdeltar7   s              r   edit_messagezOutboundMixin.edit_message\  s     B 'w// **733 	C|z99dzBBBB &**7B77h'' 		CMMNN+EE ENN6s8}}c'll   +//88L<L 	3!!%##U'( $     ,3D( 
	KKEs5zz    KK\s5zzG3S7344==QS  
 $:>>>>r   	image_urlcaptionc                   K   |st          dd          S |                    d          rX|pd                                }d| d}|r| d|                                 n|}|                     ||||	           d
{V S |                    d          r|t          d          d
         }|                     |          }	|	r |                     ||	|||           d
{V S t          dd|           S )u  Send an image reference without proactive upload.

        Three input shapes are handled in-order:
          * ``http(s)://...``    → embed as Markdown image tag (no IO)
          * ``file://...``       → strip scheme, treat as local path
          * absolute local path  → delegate to :meth:`_send_attachment`
                                   which emits a ``localfile://`` link
        Fzimage_url is requiredr   r   )zhttp://zhttps://r'   z	![image]()r   r5   r   Nfile://)r   r5   r   zImage not found: )r   r   rstriprY   r   r   _resolve_attachment_path_send_attachment)
r.   rF   r   r   r5   r   r   mdpayloadresolveds
             r   
send_imagezOutboundMixin.send_image  sf        	Le3JKKKK  788 	[Mr))++D)Y)))B37?''2''--///RG7GhQYZZZZZZZZZ 	** 	3!#i..//2I00;; 	..7!H /         
 %/N9/N/NOOOOr   	file_pathc                 |   |rt           j                            |          sdS t           j                            |          }t	          |          }t          |d          5 }t          j        |                                          	                                }ddd           n# 1 swxY w Y   ||d| d| dS )uN  Read *file_path* and wrap it as a base64 ``FileAttachment`` dict.

        Currently unused — we deliver files via ``localfile://`` Markdown
        links instead of base64 WS payloads.  Kept for potential future
        use (e.g. a small-file fast-path) and for API compatibility with
        the TS ``sendFiles`` emitter.
        Nrbzdata:z;base64,)namemimeTypebytes)
rd   re   isfilebasenamer   ry   base64	b64encodereaddecode)r.   r   	file_namemimer   r    s         r   _file_to_attachmentz!OutboundMixin._file_to_attachment  s      	y 9 9 	4G$$Y//	)$$)T"" 	7a#AFFHH--4466D	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7!t>Zd>Z>ZTX>Z>Z[[[s   "9B''B+.B+r   c                 Z  K   t                               d|||pddd                    |                     |          }|s6|                     ||          }|rt                               d||           |}|sxt                               d||                     |                     |rt          j                            |          nd}	| 	                    |d|	 d	| d
||           d{V S ||k    rt                               d||           |}	 t          j        
                    |          }
nG# t          $ r:}t                               d||           t          dd|           cY d}~S d}~ww xY w|
t          k    r"t                               d|t          |
           t          j                            |          }|pt          j                            |          }| j                            |t#                                                    |           d| dt&           | d}	 | dt)          |
           d}n# t*          $ r Y nw xY w|pd                                }|r| d|                                 n|}| 	                    ||||           d{V S )a  Deliver a local file by emitting a ``localfile://`` Markdown link.

        Never uploads.  Never embeds base64 binary.  The caller usually
        provides a local path; we normalize that exact path deterministically
        (``file://`` decode, ``~`` / env expansion, relative-path anchoring)
        without probing sibling directories, then emit an inline Markdown link
        through the standard text send path.
        z@[lightclaw] _send_attachment invoked: chat=%s file=%r caption=%rr'   N<   uG   [lightclaw] attachment path corrected via tracked write_file: %r → %rue   [lightclaw] attachment file not found: original=%r tried=%s — notifying user instead of silent dropz	(unknown)u)   ⚠️ 文件已生成但读取失败：`u   `（路径 `u   ` 不可访问）r   u/   [lightclaw] attachment path resolved: %r → %rz+[lightclaw] attachment stat failed: %r (%s)FzStat failed: r   z`[lightclaw] file %s exceeds %d bytes (got %d); advertising link anyway, download may be rejectedu   📎 [z](r   z (r   )r+   r,   r   _correct_attachment_pathr|   _attachment_candidatesrd   re   r   r   getsizer{   r   r   abspath_delivered_paths
setdefaultsetaddr   r   	Exceptionr   rY   )r.   rF   r   r   r   r5   r   resolved_path	correcteddisplaysizer   abs_pathdisplay_namelinkr   r   s                    r   r   zOutboundMixin._send_attachment  s\     " 	NYB 4	
 	
 	
 55i@@ 	*
 55gyIII *,y  
 !* 	NNE466yAA   6?Obg&&y111KGmGmmQZmmm!H #         
 I%%KKA=   "		J7??9--DD 	J 	J 	JNNH)UXYYYe3H33H3HIIIIIIIII	J /!! NND?D   7??9-- >BG$4$4X$>$> 	((#%%88<<XFFF FEE(8E(EEE	77.t44777DD 	 	 	D	 2%%''15?T%%t%%++---4YYw(XYVVVVVVVVVs0   D6 6
E: /E5/E:5E:5I 
IIc                    | sg S t          |                                           }|sg S |g}|}t          |          dk    r8|d         |d         k    r&|d         dv r|dd                                         }|                    d                              d          }|r||vr|                    |           |}|                    d          r	 t          |          }|j        dk    r9t          |j
        pd	          }|j        rt          j        d
k    rd|j         | }n'# t          $ r |t          d          d         }Y nw xY w|r||vr|                    |           t          j
                            t          j
                            |                    }|r||vr|                    |           |}|rt          j
                            |          skt          j                            d          pt          j                    }t          j
                            ||          }||vr|                    |           |r8t          j
                            |          }	|	|vr|                    |	           |S )zReturn deterministic path forms for *file_path*.

        Used only for diagnostics.  Every candidate is a direct normalization of
        the original string; no basename-based fallback probing is performed.
           r   z`"'r	   
`"',.;:)}]r   filer'   ntz//NTERMINAL_CWD)strrY   r   lstripr   appendr   r   schemer   re   netlocrd   r   r   
expandvars
expanduserisabsenvironr*   getcwdrf   realpath)
r   raw
candidatesstrippeduri_pathparsedexpandedanchoredbase_dirr   s
             r   r   z$OutboundMixin._attachment_candidates`  s     	I)nn""$$ 	I!$
x==A(1+""="=(1+QWBWBW"~++--H??6**11-@@ 	(
22h'''y)) 
	,5!(++=F**&v{'8b99H} BD#A#Ax#A#A 5 5 5#C	NNOO45 ,HJ66!!(+++7%%bg&8&8&B&BCC 	(
22h''' 	,BGMM(33 	,z~~n55DHw||Hh77Hz))!!(+++ 	,w''11Hz))!!(+++s   AD) )!EEc                     t                               |           }|sdS |D ]%}t          j                            |          r|c S &dS )zReturn the exact resolved file path if it exists, else ``None``.

        Compatible with older Hermes versions (including 0.12.0) and avoids
        directory guessing.  Only direct normalizations of the provided path are
        considered.
        N)r   r   rd   re   r   )r   r   	candidates      r   r   z&OutboundMixin._resolve_attachment_path  s_     #99)DD
 	4# 	! 	!Iw~~i(( !    !tr   c                 @   | j                             |          pg }d |D             }|sdS t          j                            |pd                                                    d                    }|D ])}t          j                            |          |k    r|c S *|                    dd          d         }|rF|D ]C}t          j                            |                              dd          d         |k    r|c S Dt          |          dk    r|d         S dS )a  Recover the real artifact path when a model-provided one is bogus.

        Weak models sometimes print a *fabricated* path in their final reply
        (wrong directory like ``/home/user/`` instead of the real
        ``/home/ubuntu/``, or a mangled double-extension like ``X.docx.md``).
        The framework's ``extract_local_files`` picks up that bare path and
        routes it here, where ``isfile()`` fails.

        The paths captured from ``write_file`` tool_start this turn
        (``_pending_file_paths``) are MODEL-INDEPENDENT ground truth, so we
        reconcile the failed request against them.  Returns the real path on a
        confident match, else ``None`` (caller then surfaces the error).
        c                 T    g | ]%}|t           j                            |          #|&S r   )rd   re   r   ).0ps     r   
<listcomp>z:OutboundMixin._correct_attachment_path.<locals>.<listcomp>  s0    BBB!!Bq0A0ABABBBr   Nr'   r   .r	   r   )	_pending_file_pathsr*   rd   re   r   rY   r   splitr   )r.   rF   r   pendingexistingreq_baser   req_stems           r   r   z&OutboundMixin._correct_attachment_path  sC     *..w77=2BBwBBB 	47##_"##%%,,];;
 

  	 	Aw""h.. /
 >>#q))!, 	  7##A&&,,S!44Q78CCHHH D
 x==AA;tr   c                    d|vrdS d}t                               |          }|r(|                    d                              d          }|s5d|v r1t                              |          }|r|                    d          }|r*|                    d          r|                    d          rdS | j                            |g           }||vr3|	                    |           t                              d||           dS dS )	zEExtract and stash the file path from a write_file tool_start message.
write_fileNr	   z	"',;:.)}]z"path"/z...z4[lightclaw] tracked write_file path: chat=%s path=%r)_WRITE_FILE_PATH_REsearchgroupr   _JSON_PATH_REr   r   r  r   r   r+   r,   )r.   rF   r&   re   mm2r  s          r   r   z$OutboundMixin._track_write_file_path  s)   w&&F"  &&w// 	3771::$$\22D  	#G++%%g..B #xx{{  	4??3// 	4==3G3G 	F*55grBBwNN4   KKF     r   c                   K   | j                             |g           }|sdS | j                            |t                                }|D ]}t          j                            |          }||v rt                              d||           Bt          j        	                    |          st                              d||           ~t                              d||           | 
                    ||           d{V  dS )al  Auto-deliver write_file results that weren't tagged with MEDIA:.

        Called by stop_typing() BEFORE emitting typing_stop so the attachment
        links land inside the same message bubble as the rest of the response.
        Paths already delivered this turn (via the framework's
        send_document() path) are skipped using ``_delivered_paths``.
        NzB[lightclaw] skip auto-deliver (already delivered): chat=%s path=%rz?[lightclaw] skip auto-deliver (file not found): chat=%s path=%rzT[lightclaw] auto-delivering write_file result (model missed MEDIA:): chat=%s path=%r)r  rM   r   r   rd   re   r   r+   r,   r   r   )r.   rF   r  	deliveredre   r   s         r   r   z$OutboundMixin._deliver_pending_files   s+      *..w;; 	F)--gsuu==	 	7 	7Dwt,,H9$$&T  
 7>>$'' UT   KK"  
 ''6666666666)	7 	7r   c           
      $  K   |rYt           j                            |          }| j                            |t                                                    |           |                     ||||||                    d                     d {V S )Nr   )r   )	rd   re   r   r   r   r   r   r   r*   )r.   rF   r   r   r   r5   r   r   s           r   send_documentzOutboundMixin.send_document"  s        	Kwy11H!,,Wcee<<@@JJJ**7Iw	S[4:JJz4J4J + L L L L L L L L 	Lr   
audio_pathc           	      l   K   |                      |||||                    d                     d {V S Nr   r   r   r*   )r.   rF   r  r   r5   r   s         r   
send_voicezOutboundMixin.send_voice0  d       **7JRZ4:JJz4J4J + L L L L L L L L 	Lr   
video_pathc           	      l   K   |                      |||||                    d                     d {V S r  r  )r.   rF   r  r   r5   r   s         r   
send_videozOutboundMixin.send_video7  r  r   
image_pathc           	      l   K   |                      |||||                    d                     d {V S r  r  )r.   rF   r  r   r5   r   s         r   send_image_filezOutboundMixin.send_image_file>  r  r   )NNrH   )NNN)NNNN)&__name__
__module____qualname____doc__r   dictr1   r   r4   r   r   listrE   rK   rO   rb   rg   r   r   r   r   r   r   r   r   r   r   r   staticmethodr   r   r   r   r   r  r  r  r  r   r   r   r   r   a   sM        &9c 9 9$ 9 9 9 9 G# GT Gd G G G G$ "& $ $( $$ $$ $ 	$
 3-$ ~$ $ $ ~$ 
$ $ $ $L(s (s ( ( ( (
3s 3t 3 3 3 38"I"I&)"I	#"I "I "I "IHGG&)G	#G G G G77 7 	7
 S#X'7 
7 7 7 7B #'-1VA VAVA VA 3-	VA
 4S>*VA VA 
VA VA VA VAv .2  4S>* 	
 
   k& k& k& k& k& k& k&f -1M? M? M?M? M? 	M? M? 4S>*M? M? 
M? M? M? M?T "&"&-1%P %P%P %P #	%P
 3-%P 4S>*%P 
%P %P %P %PV\S \Xd^ \ \ \ \( "&#'"&-1]W ]W]W ]W #	]W
 C=]W 3-]W 4S>*]W 
]W ]W ]W ]WF 3# 3$ 3 3 3 \3j C HSM    \<++'*+	#+ + + +Zc C D    < 7C  7D  7  7  7  7F FJCGL LL'*L5=c]LC=L3;C=L 
L L L L GK"&L LL(+L6>smL3-L 
L L L L GK"&L LL(+L6>smL3-L 
L L L L GK"&L LL(+L6>smL3-L 
L L L L L Lr   r   )*r#  r   ru   loggingrd   rerA   typingr   r   r   urllib.parser   r   gateway.platforms.baser   configr
   r   r   r   r   r   r   r   mediar   	getLoggerr   r+   compileUNICODEr   DOTALLr
  r  r   tupler   __annotations__r   r   r   r   r   <module>r4     s    <    				 				  & & & & & & & & & & * * * * * * * * - - - - - -	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 $ # # # # #		8	$	$ BJ2BJ   !bj0I   
566 $NsCx M M Ms s    bL bL bL bL bL bL bL bL bL bLr   