
    (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)dS ) 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dS |                                  }|rt          |          dk    rdS t          t                              |                    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     6/home/rurouni/.hermes/hermes-agent/gateway/delivery.py_is_silence_narrationr   +   sX      u}}H s8}}r))u"((22333       )PlatformGatewayConfig)SessionSource)DeadTargetRegistrychat_idc                 b    | dS 	 t          |           dk    S # t          t          f$ r Y dS 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.
    NFr   int	TypeError
ValueError)r   s    r   #looks_like_telegram_private_chat_idr#   >   sK     u7||az"   uus    ..valuec                 ^    | dS 	 t          |            dS # t          t          f$ r Y dS w xY w)NFTr   )r$   s    r   _looks_like_intr&   N   sH    }uE


tz"   uus    ,,resultc                     t          | t                    r|                     d          du S t          | dd          du S )NsuccessFT)
isinstancedictgetgetattr)r'   s    r   _send_result_failedr.   X   sB    &$ .zz)$$--69d++u44r   c                     t          | t                    r|                     d          }nt          | dd           }|rt	          |          nd S )Nerrorr*   r+   r,   r-   strr'   r0   s     r   _send_result_errorr4   ^   sM    &$ /

7##..(3u:::D(r   c                 j    t          |           }t          |od|                                v           S )Nzthread not found)r4   r   lowerr3   s     r   #_is_thread_not_found_delivery_errorr7   f   s0    v&&E=,=>>>r   c                     t          | t                    r|                     d          }nt          | dd          }|rt	          |          ndS )zFReturn the machine-readable error_kind from a SendResult/dict, if any.
error_kindNr1   )r'   kinds     r   _send_result_error_kindr;   k   sM    &$ 3zz,''v|T22&3t999$&r   
error_textc                     | sdS 	 ddl m}m} n# t          $ r Y dS w xY w |d|           }t	          j        |          sdS |dk    r ||           sdS |S )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      tPPPPPPPPP   ttt
;;;D066 t
 {#:#:j#Q#Q#QtKs    
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dS )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                    |                                 }|                                }|dk    r7|r | |j        |j        |j        d          S  | t
          j        d          S |dk    r | t
          j                  S d|v r|                    dd          }|d	                                         }t          |          d
k    r|d
         nd}t          |          dk    r|d         nd}	 t          |          }	 | |	||d          S # t          $ r  | t
          j                  cY S w xY w	 t          |          }	 | |	          S # 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   s    !,,..&,,..8## 	Ds#_"N$."	    sHNdCCCC7""3//// /!!#))#q11E 8>>++L"%e**q..eAhhdG$'JJNNaI4#L11sHg`deeee 4 4 4sHN3333334
	0--H3)))) 	0 	0 	03//////	0s$   1D  D21D26E  E43E4c                     | j         rdS | j        t          j        k    rdS | j        r%| j        r| 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    > 	8=HN**7< 	LDN 	Lm)KKDLKK4>KKK< 	;m)::DL:::}""r   )N)__name__
__module____qualname____doc__r   __annotations__r   r   r2   rH   rI   r   rJ   classmethodr   rX   r[    r   r   rF   rF      s           !GXc]!!!#Ix}###ItK00 003 00(? 00K[ 00 00 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dS )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                     || _         |pi | _        t                      dz  dz  | _        |pt	                      | _        dS )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__   sG      B)++f4x?(@,>,@,@r   r   targetsjob_idjob_namemetadatar   c                    K   i }|D ]}|j         t          j        k    rx|j        rq| j                            |j         j        |j                  rGt                              d|j         j        |j                   dddd||	                                <   	 |j         t          j        k    r| 
                    ||||          }n]|                     |||           d{V }|j        r9t          |          s*| j                            |j         j        |j                   d|d||	                                <   8# t          $ r}	|j         t          j        k    rk|j        rdt          t!          |	                    }
|
rF| j                            |j         j        |j        |
 d	t!          |	          dd
                     dt!          |	          d||	                                <   Y d}	~	d}	~	ww xY w|S )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   sK     *  0	 0	F 8>11N 2%--fo.CV^TT 2 2O)6>    %,F/ /((**+
 ?hn44!00&(HUUFF#'#<#<VWh#W#WWWWWWWF~ W.A&.I.I W)//0Ev~VVV  $$/ /((**++     ?hn444 >s1vv F FI  )33"O16>&/#A#A3q66$3$<#A#A 4   
  % VV/ /((**++++++ s   B%D??
G;	B'G66G;c                    t          j                                        d          }|r| j        |z  | dz  }n| j        dz  | dz  }|j                            dd           g }|r|                    d|            n|                    d           |                    d           |                    d	t          j                                        d
                      |r|                    d|            |r5|                                D ] \  }}	|                    d| d|	            !|                    d           |                    d           |                    d           |                    |           |                    d	                    |                     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  s    LNN++O<<	 	G/F2	5F5F5FFKK/F2	5F5F5FFK   ===  	.LLh))))LL,---RUx|~~'>'>?R'S'SUUVVV 	2LL000111 	4&nn.. 4 4
U2#225223333RURWtyy//000 $$"
 
 	
r   c                     t          j                                        d          }t                      dz  dz  }|                    dd           || d| dz  }|                    |           |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  sx    LNN++O<<	!##f,x7dT222F44Y4444   r   c                     t          j        d          }|(|                                                                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_NARRATIONN)1trueyesonfilter_silence_narrationT)osgetenvr   r6   r   r-   re   )rZ   envs     r   !_filter_silence_narration_enabledz0DeliveryRouter._filter_silence_narration_enabledx  sV     i9::?99;;$$&&*DDDGDK)CTJJKKKr   rK   c                   K   | j                             |j                  }|st          d|j        j                   |j        st          d|j        j         d          |pi                     dd          }d}t          |          t          k    r	 |                     ||          }nA# t          $ r4}t                              dt          |          ||           Y d}~nd}~ww xY wt          |dd	          r,|r)t                              d
t          |          |           ny||                     ||          }d| d}t          dt          t          |          z
            }	t                              dt          |          |           |d|	         |z   }|                                 rIt!          |          r:t                              d|j        j        |j        |dd                    ddd	dS t#          |pi           }
d	}d}|j        r2d|
v pd|
v }|j        }|j        t&          j        k    o.t+          |j                  ot-          |           o
d|
vod|
vo| }|rj|}t          |dd          }|t/          d           ||j        |           d{V }|st/          d| d          t1          |          }||
d<   d|
d<   ns|j        t&          j        k    rOt+          |j                  r;d|
vr7d|
vr3|s1|
                    d          }|t/          d          ||
d<   d|
d<   nd|
vrd|
vr|s||
d<   |                    |j        ||
pd            d{V }t5          |          r|r|rt7          |          rt          |dd          }|t/          d!           ||j        |d"           d{V }|st/          d#| d          t1          |          |
d<   d|
d<   |                    |j        ||
pd            d{V }t5          |          r+t/          t9          |          p|j        j         d$          |S )%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      -##FO44 	SQ&/:OQQRRR~ 	QOv/DOOOPPP .b%%h	::%)
w<<---
!33GVDD

   ;LL&#        w 6>> 5 KK2Gj   %!%!7!7!H!HJRZRRRa!4s6{{!BCCJLL*   "(7(+f4 1133 	8Mg8V8V 	NNH%	    /"   X^,,*/';?) 1	>*m; H6-G &  &/8#44 27GG2'(89992  }42 (}<	2
 21 , / #>4D1")'3Dd"K"K"*&P   +:/&.JZ*[*[$[$[$[$[$[$[!( &ZGWZZZ   $''8#9#9 -=k*FJBCC8#4447GG 5}44'}<<1 =  -001OPP'&J   .>k*DH@AAM116IQ^6^6^  hA6^-=k*||FNGmF[W[|\\\\\\\\v&& 	m/e5e 8??e
 #*'3Dd"K"K"*&Q   -<ON5!%- - - ' ' ' ' ' '#
 + &lHilll   .11D-E-Ek*FJBC&||FNGmNc_c|dddddddd"6** m"#5f#=#=#kFODYAkAkAkllls   B/ /
C-9*C((C-)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      s         OS>BA A} AXs]8K A'(:;A A A A( !%"&-1I II n%I 	I
 3-I 4S>*I 
c3hI I I IV,
,
 ,
 3-	,

 4S>*,
 
c3h,
 ,
 ,
 ,
\ c d    
L4 
L 
L 
L 
Lee e 4S>*	e
 
c3he e e e e 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      s     				 				             ! ! ! ! ! ! , , , , , , , , , , , , - - - - - -		8	$	$    RZ2M  48C= 4T 4 4 4 4 , + + + + + + + " " " " " " , , , , , ,# 4     8C= T    5 5 5 5 5 5)s )x} ) ) ) )? ? ? ? ? ?
'C 'HSM ' ' ' 'x} #    2 M# M# M# M# M# M# M# M#`K K K K K K K K K Kr   