
    `gjZ                        d Z ddlZddlZddlZddlmZ ddlmZ ddlmZ ddl	m
Z
mZmZmZ ddlmZ  ej                   e      ZdZ ej(                  d	ej*                        Zd
ee   defdZddlmZmZ ddlmZ ddlm Z  dee   defdZ!dee   defdZ"dedefdZ#dedee   fdZ$dedefdZ%dedee   fdZ&dee   dee   fdZ'e G d d             Z( G d d      Z)y) u0  
Delivery routing for cron job outputs and agent responses.

Routes messages to the appropriate destination based on:
- Explicit targets (e.g., "telegram:123456789")
- Platform home channels (e.g., "telegram" → home channel)
- Origin (back to where the job was created)
- Local (always saved to files)
    N)Path)datetime)	dataclass)DictListOptionalAny)get_hermes_homei  zv^[\s*_~`]*\(?\s*(silent|silence|no\s+response|no\s+reply)\s*\.?\)?[\s*_~`]*$|^[\s*_~`]*[\U0001F507\.\u2026]+[\s*_~`]*$contentreturnc                     | sy| j                         }|rt        |      dkD  ryt        t        j	                  |            S )u  Return True when ``content`` is *only* a silence-narration token.

    Length-guarded (real messages are longer) and anchored to the whole string
    so legitimate prose like "The deployment ran silently" or "Silence is
    golden — here is the plan..." is never flagged.
    F@   )striplenbool_SILENCE_NARRATIONmatch)r   strippeds     C/root/.hermes/venv/lib/python3.12/site-packages/gateway/delivery.py_is_silence_narrationr   +   s=     }}Hs8}r)"((233       )PlatformGatewayConfig)SessionSource)DeadTargetRegistrychat_idc                 P    | y	 t        |       dkD  S # t        t        f$ r Y yw xY w)uu  True when ``chat_id`` is a positive int — Telegram's private-chat shape.

    Telegram private chats use positive chat IDs; groups/channels/supergroups
    use negative IDs. This is the single source of truth for that heuristic,
    reused by the handoff seed path in ``gateway/run.py`` so handoff-created
    DM topics key the same way as inbound DM-topic messages.
    Fr   int	TypeError
ValueError)r   s    r   #looks_like_telegram_private_chat_idr#   >   s7     7|az" s    %%valuec                 L    | y	 t        |        y# t        t        f$ r Y yw xY w)NFTr   )r$   s    r   _looks_like_intr&   N   s0    }E
z" s    ##resultc                 f    t        | t              r| j                  d      du S t        | dd      du S )NsuccessFT)
isinstancedictgetgetattr)r'   s    r   _send_result_failedr.   X   s4    &$zz)$--69d+u44r   c                 ~    t        | t              r| j                  d      }nt        | dd       }|rt	        |      S d S )Nerrorr*   r+   r,   r-   strr'   r0   s     r   _send_result_errorr4   ^   s9    &$

7#.3u:(D(r   c                 V    t        |       }t        |xr d|j                         v       S )Nzthread not found)r4   r   lowerr3   s     r   #_is_thread_not_found_delivery_errorr7   f   s(    v&E=,=>>r   c                 ~    t        | t              r| j                  d      }nt        | dd      }|rt	        |      S dS )zFReturn the machine-readable error_kind from a SendResult/dict, if any.
error_kindNr1   )r'   kinds     r   _send_result_error_kindr;   k   s9    &$zz,'v|T23t9&$&r   
error_textc                     | sy	 ddl m}m}  |d|       }t	        j
                  |      sy|dk(  r
 ||       sy|S # t        $ r Y yw xY w)a3  Best-effort dead-target classification from a raised error's text.

    ``_deliver_to_platform`` raises (it does not return a SendResult) on a hard
    failure, so the ``deliver()`` loop only has the exception string.  Reuse the
    platform-neutral classifier to recover the error_kind from that text.
    Nr   )classify_send_erroris_chat_level_not_found)r<   	not_found)platforms.baser>   r?   	Exceptionr   is_dead_error_kind)r<   r>   r?   r:   s       r   _classify_dead_from_error_textrD   t   s_     P t
;D006
 {#:j#QK  s   > 	A
	A
c                       e Zd ZU dZeed<   dZee   ed<   dZ	ee   ed<   dZ
eed<   dZeed<   edd	ed
ee   dd fd       ZdefdZy)DeliveryTargetu   
    A single delivery target.
    
    Represents where a message should be sent:
    - "origin" → back to source
    - "local" → save to local files
    - "telegram" → Telegram home channel
    - "telegram:123456" → specific Telegram chat
    platformNr   	thread_idF	is_originis_explicittargetoriginr   c                    |j                         }|j                         }|dk(  rD|r* | |j                  |j                  |j                  d      S  | t
        j                  d      S |dk(  r | t
        j                        S d|v rg|j                  dd      }|d	   j                         }t        |      d
kD  r|d
   nd}t        |      dkD  r|d   nd}	 t        |      }	 | |	||d      S 	 t        |      }	 | |	      S # t        $ r  | t
        j                        cY S w xY w# t        $ r  | t
        j                        cY S w xY w)u   
        Parse a delivery target string.
        
        Formats:
        - "origin" → back to source
        - "local" → local files only
        - "telegram" → Telegram home channel
        - "telegram:123456" → specific Telegram chat
        rL   T)rG   r   rH   rI   )rG   rI   local)rG   :   r   r   N)rG   r   rH   rJ   )
r   r6   rG   r   rH   r   LOCALsplitr   r"   )
clsrK   rL   target_strippedtarget_lowerpartsplatform_strr   rH   rG   s
             r   parsezDeliveryTarget.parse   sI    !,,.&,,.8##__"NN$.."	  HNNdCC7"// /!#))#q1E 8>>+L"%e*q.eAhdG$'JNaI4#L1Hg`dee	0-H))  4HNN334  	0//	0s$   D 2D,  D)(D), EEc                 z   | j                   ry| j                  t        j                  k(  ry| j                  r>| j
                  r2| j                  j                   d| j                   d| j
                   S | j                  r%| j                  j                   d| j                   S | j                  j                  S )zConvert back to string format.rL   rN   rO   )rI   rG   r   rQ   r   rH   r$   )selfs    r   	to_stringzDeliveryTarget.to_string   s    >>==HNN*<<DNNmm))*!DLL>4>>:JKK<<mm))*!DLL>::}}"""r   )N)__name__
__module____qualname____doc__r   __annotations__r   r   r2   rH   rI   r   rJ   classmethodr   rX   r[    r   r   rF   rF      sy     !GXc]!#Ix}#ItK003 00(? 00K[ 00 00d
#3 
#r   rF   c                   "   e Zd ZdZ	 	 ddedeeef   dee	   fdZ
	 	 	 ddedee   d	ee   d
ee   deeeef      deeef   fdZded	ee   d
ee   deeeef      deeef   f
dZded	edefdZdefdZdededeeeef      deeef   fdZy)DeliveryRouterz
    Routes messages to appropriate destinations.
    
    Handles the logic of resolving delivery targets and dispatching
    messages to the right platform adapters.
    Nconfigadaptersdead_targetsc                 x    || _         |xs i | _        t               dz  dz  | _        |xs
 t	               | _        y)aS  
        Initialize the delivery router.
        
        Args:
            config: Gateway configuration
            adapters: Dict mapping platforms to their adapter instances
            dead_targets: Optional shared registry of confirmed-unreachable
                targets.  When omitted, a profile-local registry is created.
        cronoutputN)re   rf   r
   
output_dirr   rg   )rZ   re   rf   rg   s       r   __init__zDeliveryRouter.__init__   s;      B)+f4x?(@,>,@r   r   targetsjob_idjob_namemetadatar   c                 v  K   i }|D ]h  }|j                   t        j                  k7  r|j                  r| j                  j                  |j                   j                  |j                        rMt        j                  d|j                   j                  |j                         dddd||j                         <   	 |j                   t        j                  k(  r| j                  ||||      }nl| j                  |||       d{   }|j                  rEt        |      s:| j                  j                  |j                   j                  |j                         d|d||j                         <   k |S 7 p# t        $ r}	|j                   t        j                  k7  ro|j                  rct        t!        |	            }
|
rM| j                  j#                  |j                   j                  |j                  |
 d	t!        |	      dd
         dt!        |	      d||j                         <   Y d}	~	*d}	~	ww xY ww)a  
        Deliver content to all specified targets.
        
        Args:
            content: The message/output to deliver
            targets: List of delivery targets
            job_id: Optional job ID (for cron jobs)
            job_name: Optional job name
            metadata: Additional metadata to include
        
        Returns:
            Dict with delivery results per target
        zHSkipping delivery to known-dead target %s:%s (send to it again to clear)Fdead_targetz'target previously confirmed unreachable)r)   skippedr0   NT)r)   r'   z: x   )reason)r)   r0   )rG   r   rQ   r   rg   is_deadr$   loggerinfor[   _deliver_local_deliver_to_platformr.   clearrB   rD   r2   	mark_dead)rZ   r   rm   rn   ro   rp   resultsrK   r'   e	dead_kinds              r   deliverzDeliveryRouter.deliver   s    *  0	F 8>>1NN%%--foo.C.CV^^T2OO))6>>  %,F/((*+
 ??hnn4!00&(HUF#'#<#<VWh#WWF~~.A&.I))//0E0Ev~~V  $$/((*+?0	d 1 X   ??hnn4 >s1v FI ))33"OO116>>&/[3q6$3<.#A 4 
  % V/((*+sE   B9H9<AE7E5A+E70H95E77	H6 B+H1+H91H66H9c                 .   t        j                         j                  d      }|r| j                  |z  | dz  }n| j                  dz  | dz  }|j                  j                  dd       g }|r|j                  d|        n|j                  d       |j                  d       |j                  d	t        j                         j                  d
              |r|j                  d|        |r/|j                         D ]  \  }}	|j                  d| d|	         |j                  d       |j                  d       |j                  d       |j                  |       |j                  dj                  |             t        |      |dS )zSave content to local files.%Y%m%d_%H%M%Sz.mdmiscTparentsexist_okz# z# Delivery Output z**Timestamp:** z%Y-%m-%d %H:%M:%Sz**Job ID:** z**z:** z---
)path	timestamp)r   nowstrftimerk   parentmkdirappenditems
write_textjoinr2   )
rZ   r   rn   ro   rp   r   output_pathlineskeyr$   s
             r   ry   zDeliveryRouter._deliver_localA  sq    LLN++O<	//F2	{#5FFK//F2	{#5FFK   = LL2hZ)LL,-Rx||~'>'>?R'S&TUVLL<x01&nn. 4
Ur#d5'234 	RURWtyy/0 $"
 	
r   c                     t        j                         j                  d      }t               dz  dz  }|j	                  dd       || d| dz  }|j                  |       |S )z7Save full cron output to disk and return the file path.r   ri   rj   Tr   _z.txt)r   r   r   r
   r   r   )rZ   r   rn   r   out_dirr   s         r   _save_full_outputz DeliveryRouter._save_full_outputo  sb    LLN++O<	!#f,x7dT2F81YKt44 r   c                     t        j                  d      }| |j                         j                         dv S t	        t        | j                  dd            S )zWhether the outbound silence-narration filter is active.

        ``HERMES_FILTER_SILENCE_NARRATION`` env var overrides config when set;
        otherwise the ``gateway.filter_silence_narration`` config flag wins
        (default True).
        HERMES_FILTER_SILENCE_NARRATION)1trueyesonfilter_silence_narrationT)osgetenvr   r6   r   r-   re   )rZ   envs     r   !_filter_silence_narration_enabledz0DeliveryRouter._filter_silence_narration_enabledx  sL     ii9:?99;$$&*DDDGDKK)CTJKKr   rK   c                   K   | j                   j                  |j                        }|s"t        d|j                  j                         |j
                  s#t        d|j                  j                   d      |xs i j                  dd      }d}t        |      t        kD  r	 | j                  ||      }t        |dd	      r#|rt        j                  d
t        |      |       n^|| j                  ||      }d| d}t        dt        t        |      z
        }	t        j                  dt        |      |       |d|	 |z   }| j                         rJt!        |      r?t        j                  d|j                  j                  |j
                  |dd        ddd	dS t#        |xs i       }
d	}d}|j$                  r=d|
v xs d|
v }|j$                  }|j                  t&        j(                  k(  xr4 t+        |j
                        xr t-        |       xr d|
vxr	 d|
vxr | }|r^|}t        |dd      }|t/        d       ||j
                  |       d{   }|st/        d| d      t1        |      }||
d<   d|
d<   nt|j                  t&        j(                  k(  rHt+        |j
                        r3d|
vr/d|
vr+|s)|
j                  d      }|t/        d      ||
d<   d|
d<   nd|
vrd|
vr|s||
d<   |j3                  |j
                  ||
xs d        d{   }t5        |      r|r|rt7        |      rt        |dd      }|t/        d!       ||j
                  |d"       d{   }|st/        d#| d      t1        |      |
d<   d|
d<   |j3                  |j
                  ||
xs d        d{   }t5        |      r/t/        t9        |      xs |j                  j                   d$      |S # t        $ r,}t        j                  dt        |      ||       Y d}~zd}~ww xY w7 7 7 7 w)%z(Deliver content to a messaging platform.zNo adapter configured for zNo chat ID for z	 deliveryrn   unknownNua   Audit save failed for cron output (%d chars, job=%s): %s — delivery proceeds without audit copysplits_long_messagesFuQ   Cron output preserved for chunking adapter (%d chars) — full output saved to %sz'

... [truncated, full output saved to ]r   u4   Cron output truncated (%d chars) — full output: %sz6Dropped silence-narration outbound to %s (chat=%s): %r(   Tsilence_narration)r)   filtered	delivereddirect_messages_topic_id!telegram_direct_messages_topic_idrH   message_thread_idensure_dm_topicz6Telegram adapter cannot create named private DM topicsz,Failed to create Telegram private DM topic ''"telegram_dm_topic_created_for_sendtelegram_reply_to_message_idzyTelegram private DM topic delivery requires telegram_reply_to_message_id; send to the bare chat or provide a reply anchor telegram_dm_topic_reply_fallback)rp   z7Telegram adapter cannot refresh named private DM topics)force_createz-Failed to refresh Telegram private DM topic 'z delivery failed)rf   r,   rG   r"   r$   r   r   MAX_PLATFORM_OUTPUTr   OSErrorrw   warningr-   rx   maxr   r   r+   rH   r   TELEGRAMr#   r&   RuntimeErrorr2   sendr.   r7   r4   )rZ   rK   r   rp   adapterrn   
saved_pathexcfootervisiblesend_metadatais_named_telegram_private_topic!named_telegram_private_topic_namehas_explicit_direct_topictarget_thread_idr   created_thread_idreply_anchorr'   refreshed_thread_ids                       r   rz   z#DeliveryRouter._deliver_to_platform  s     --##FOO49&//:O:O9PQRR~~v/D/D.EYOPP .b%%h	:%)
w<--
!33GVD
 w 6>KK2Gj %!%!7!7!HJDZLPQRa!4s6{!BCJL* "(7+f4 1138Mg8VNNH%%	  /"  X^,*/';?)*m; H6-G &  &//8#4#44 27G2'(8992  }42 (}<	2
 21 , /4D1")'3Dd"K"*&P  +:&..JZ*[$[!(&FGWFXXYZ  $''8#9 -=k*FJBC8#4#447G}4'}<1  -001OP'&J  .>k*DH@AM16IQ^6^  hA-=k*||FNNGmF[W[|\\v&/57?")'3Dd"K"*&Q  -<NN5!%- '#
 +&GHiGjjkl  .11D-Ek*FJBC&||FNNGmNc_c|dd"6*"#5f#=#kFOODYDYCZZjAkllE  ;L&# V %\: ]' esw   B#Q&P 8F%QP:CQ"P=#AQ3Q 4AQQA Q	P7!P2,Q2P77Q=Q QQ)NN)NNN)r\   r]   r^   r_   r   r   r   r	   r   r   rl   r2   r   rF   r   ry   r   r   r   r   rz   rb   r   r   rd   rd      ss    OS>BA} AXs]8K A'(:;A( !%"&-1II n%I 	I
 3-I 4S>*I 
c3hIV,
,
 ,
 3-	,

 4S>*,
 
c3h,
\ c d 
L4 
Lee e 4S>*	e
 
c3her   rd   )*r_   loggingr   repathlibr   r   dataclassesr   typingr   r   r   r	   hermes_cli.configr
   	getLoggerr\   rw   r   compile
IGNORECASEr   r2   r   r   re   r   r   sessionr   rg   r   r#   r&   r.   r4   r7   r;   rD   rF   rd   rb   r   r   <module>r      sH    	 	   ! , , -			8	$    RZZ2MM 48C= 4T 4 , " ,# 4  8C= T 5 5 5)s )x} )? ? ?
'C 'HSM 'x} # 2 M# M# M#`K Kr   