
    Tdjf              	       |   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mZ ddlmZ ddl m!Z!  ed           G d d                      Z"dededee
eef                  dee"         fd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/thesage/.hermes/hermes-agent/gateway/delivery.py_is_silence_narrationr   +   sX      u}}H s8}}r))u"((22333       )PlatformGatewayConfigPlatformConfig)SessionSource)DeadTargetRegistryT)frozenc                       e Zd ZU dZeed<   ee         ed<   eed<   e	de
fd            Zdeded	ed
eeeef                  def
dZdS )DeliveryTransportz:Resolved live transport for one logical delivery platform.adapterconfigtransport_platformr   c                 ,    | j         t          j        k    S N)r#   r   RELAYselfs    r   is_relayzDeliveryTransport.is_relayF   s    &(.88r   logical_platformchat_idr   metadatac                    K   | j         r$| j                            ||||           d{V S | j                            |||           d{V S )zBSend through this transport while preserving the logical platform.r,   N)r)   r!   send_for_platformsend)r(   r*   r+   r   r,   s        r   r0   zDeliveryTransport.sendJ   s       = 	77 !	 8          \&&w(&KKKKKKKKKr   N)__name__
__module____qualname____doc__r	   __annotations__r   r   r   propertyr   r)   strr   r0    r   r   r    r    >   s         DDLLL^$$$$    9$ 9 9 9 X9L"L L 	L
 4S>*L 
L L L L L Lr   r    platformr"   adaptersc                    |pi }|                     |           }|j                             |           }|||j        rt          |||           S |                     t          j                  }|j                             t          j                  }t          |dd          }|?||j        r6t          |          r' ||           rt          ||t          j                  S dS )ad  Resolve a logical platform to its live delivery transport.

    A concrete native adapter always wins. Relay is eligible only when its
    authenticated transport explicitly advertises that it fronts the logical
    platform, which keeps restart-time delivery independent of per-chat caches
    without letting Relay hijack unrelated platform targets.
    N)r!   r"   r#   fronts_platform)get	platformsenabledr    r   r&   getattrcallable)	r9   r"   r:   live_adaptersnativenative_configrelayrelay_configr<   s	            r   resolve_delivery_transportrG   \   s    NMx((F$((22M }48M4  '
 
 
 	
 hn--E#''77Le%6==O!\%9!_%% "OH%% " !'~
 
 
 	

 4r   r+   c                 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_idrM      sK     u7||az"   uus    ..valuec                 ^    | dS 	 t          |            dS # t          t          f$ r Y dS w xY w)NFTrI   )rN   s    r   _looks_like_intrP      sH    }uE


tz"   uus    ,,resultc                     t          | t                    r|                     d          du S t          | dd          du S )NsuccessFT)
isinstancedictr=   r@   )rQ   s    r   _send_result_failedrV      sB    &$ .zz)$$--69d++u44r   c                     t          | t                    r|                     d          }nt          | dd           }|rt	          |          nd S )NerrorrT   rU   r=   r@   r7   rQ   rX   s     r   _send_result_errorr[      sM    &$ /

7##..(3u:::D(r   c                 j    t          |           }t          |od|                                v           S )Nzthread not found)r[   r   lowerrZ   s     r   #_is_thread_not_found_delivery_errorr^      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_kindNrY   )rQ   kinds     r   _send_result_error_kindrb      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)rc   	not_found)platforms.basere   rf   	Exceptionr   is_dead_error_kind)rc   re   rf   ra   s       r   _classify_dead_from_error_textrk      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
    r9   Nr+   	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
        rr   T)r9   r+   rn   ro   )r9   ro   local)r9   :   r   r   N)r9   r+   rn   rp   )
r   r]   r9   r+   rn   r   LOCALsplitr   rL   )
clsrq   rr   target_strippedtarget_lowerpartsplatform_strr+   rn   r9   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.rr   rt   ru   )ro   r9   r   rw   r+   rn   rN   r'   s    r   	to_stringzDeliveryTarget.to_string  s    > 	8=HN**7< 	LDN 	Lm)KKDLKK4>KKK< 	;m)::DL:::}""r   r%   )r1   r2   r3   r4   r   r5   r+   r   r7   rn   ro   r   rp   classmethodr   r~   r   r8   r   r   rm   rm      s           !GXc]!!!#Ix}###ItK00 003 00(? 00K[ 00 00 00 [00d
#3 
# 
# 
# 
# 
# 
#r   rm   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.
    Nr"   r:   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)r"   r:   r
   
output_dirr   r   )r(   r"   r:   r   s       r   __init__zDeliveryRouter.__init__.  sG      B)++f4x?(@,>,@,@r   r   targetsjob_idjob_namer,   r   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)rS   skippedrX   NT)rS   rQ   z: x   )reason)rS   rX   )r9   r   rw   r+   r   is_deadrN   loggerinfor   _deliver_local_deliver_to_platformrV   clearri   rk   r7   	mark_dead)r(   r   r   r   r   r,   resultsrq   rQ   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	                    |          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---
utf-8encoding)path	timestamp)r   nowstrftimer   parentmkdirappenditems
write_textjoinr7   )
r(   r   r   r   r,   r   output_pathlineskeyrN   s
             r   r   zDeliveryRouter._deliver_local  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//'BBB $$"
 
 	
r   c                     t          j                                        d          }t                      dz  dz  }|                    dd           || d| dz  }|                    |d	           |S )
z7Save full cron output to disk and return the file path.r   r   r   Tr   _z.txtr   r   )r   r   r   r
   r   r   )r(   r   r   r   out_dirr   s         r   _save_full_outputz DeliveryRouter._save_full_output  s|    LNN++O<<	!##f,x7dT222F44Y4444'222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   r]   r   r@   r"   )r(   envs     r   !_filter_silence_narration_enabledz0DeliveryRouter._filter_silence_narration_enabled  sV     i9::?99;;$$&&*DDDGDK)CTJJKKKr   rq   c                 	  K   t          |j        | j        | j                  }|t	          d|j        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           }|j        rS| j                            |j                  }|2|j        |j        k    r"|j        r
|j        |d<   |j        r
|j        |d<   d	}d}|j        r2d|v pd|v }|j        }|j        t4          j        k    o.t9          |j                  ot;          |           o
d|vod|vo| }|rj|}t          |dd          }|t=          d           ||j        |           d{V }|st=          d| d          t?          |          }||d<   d|d<   ns|j        t4          j        k    rOt9          |j                  r;d|vr7d|vr3|s1|                    d          }|t=          d           ||d<   d|d!<   nd|vrd|vr|s||d<   |                     |j        |j        ||pd"           d{V }tC          |          r|r|rtE          |          rt          |dd          }|t=          d#           ||j        |d$           d{V }|st=          d%| d          t?          |          |d<   d|d<   |                     |j        |j        ||pd"           d{V }tC          |          r+t=          tG          |          p|j        j         d&          |S )'z(Deliver content to a messaging platform.NzNo adapter configured for zNo chat ID for z	 deliveryr   unknownua   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)rS   filtered	delivereduser_idscope_iddirect_messages_topic_id!telegram_direct_messages_topic_idrn   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_fallbackr.   z7Telegram adapter cannot refresh named private DM topics)force_createz-Failed to refresh Telegram private DM topic 'z delivery failed)$rG   r9   r"   r:   rL   rN   r!   r+   r=   r   MAX_PLATFORM_OUTPUTr   OSErrorr   warningr@   r   maxr   r   rU   r)   get_home_channelr   r   rn   r   TELEGRAMrM   rP   RuntimeErrorr7   r0   rV   r^   r[   )r(   rq   r   r,   	transportr!   r   
saved_pathexcfootervisiblesend_metadatahomeis_named_telegram_private_topic!named_telegram_private_topic_namehas_explicit_direct_topictarget_thread_idr   created_thread_idreply_anchorrQ   refreshed_thread_ids                         r   r   z#DeliveryRouter._deliver_to_platform  s      /vT][[	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^,, 	>;//@@DDLFN$B$B< </3|M),= >04M*-*/';?) 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* ~~ON"*d	 & 
 
 
 
 
 
 
 
 v&& 	m/5 8??
 #*'3Dd"K"K"*&Q   -<ON5!%- - - ' ' ' ' ' '#
 + &lHilll   .11D-E-Ek*FJBC(~~ON*2d	  .           #6** m"#5f#=#=#kFODYAkAkAkllls    B7 7
C5*C00C5)NN)NNN)r1   r2   r3   r4   r   r   r   r	   r   r   r   r7   r   rm   r   r   r   r   r   r   r   r8   r   r   r   r   &  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vv v 4S>*	v
 
c3hv v v v v vr   r   )-r4   loggingr   repathlibr   r   dataclassesr   typingr   r   r   r	   hermes_cli.configr
   	getLoggerr1   r   r   compile
IGNORECASEr   r7   r   r   r"   r   r   r   sessionr   r   r   r    rG   rM   rP   rV   r[   r^   rb   rk   rm   r   r8   r   r   <module>r      sR     				 				             ! ! ! ! ! ! , , , , , , , , , , , , - - - - - -		8	$	$    RZ2M  48C= 4T 4 4 4 4 < ; ; ; ; ; ; ; ; ; " " " " " " , , , , , , $L L L L L L L L:''' tHcM*+'  	' ' ' 'T# 4     8C= T    5 5 5 5 5 5)s )x} ) ) ) )? ? ? ? ? ?
'C 'HSM ' ' ' 'x} #    2 M# M# M# M# M# M# M# M#`\ \ \ \ \ \ \ \ \ \r   