
    epj                   l   U d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlZddl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 ddlmZ erddlZdZn	 ddlZdZn# e$ r d	ZdZY nw xY wdd
lmZm Z  ddl!m"Z"m#Z#m$Z$m%Z%m&Z& ddl'm(Z(m)Z) ddl*m+Z+ ddl,m-Z. ddl,m/Z0 ddZ1 ej2        e3          Z4dZ5dZ6dZ7dZ8ddZ9ddZ:dd!Z;dd"Z<dd%Z=d&Z>d'Z?d(Z@dd)lAmBZCmDZD daEd*eFd+<   dZGd*eFd,<   d-ZHdd.ZIdd/ZJd0ZKd1ZLd2ZMd3ZNd4ZOd5d6gZP G d7 d8eQ          ZR	 ddd=ZSd>ZTddAZUddBZVddDZWddEZXddGZYddJZZddKZ[ddLZ\ddMZ]ddPZ^ddQZ_ddSZ`ddTZaddVZbddWZcddZZddd[Zedd]Zfdd^Zgdd_Zh G d` dae"          ZidddZjdedfdgdhdededediZkdjdjdkdldmdndndoZld	dpddsZmdduZnddd	dvdd~ZoddZpdS )u  
Photon Spectrum (iMessage) platform adapter for Hermes Agent.

Both directions of traffic flow through a small supervised Node sidecar
(see ``sidecar/index.mjs``) that runs the ``spectrum-ts`` SDK — the SDK is
TypeScript-only and there is no public HTTP message API, so a sidecar is
unavoidable.

Inbound:
    The SDK's ``app.messages`` is a long-lived **gRPC** stream. The sidecar
    serializes each message to a normalized JSON event and streams it to this
    adapter over a loopback ``GET /inbound`` (NDJSON). A background task here
    consumes that stream, dedupes on ``messageId``, and dispatches a
    ``MessageEvent`` to the gateway via ``BasePlatformAdapter.handle_message``.
    No webhook, no public URL, no signing secret.

Outbound:
    ``send`` / ``send_typing`` are loopback POSTs to the sidecar's control
    endpoints, authenticated with a shared bearer token. URL-only messages can
    route through spectrum-ts' ``richlink()`` builder via ``/send-richlink``.
    Outbound media (images, voice notes, video, documents) goes through
    spectrum-ts' ``attachment()`` / ``voice()`` content builders via the
    sidecar's ``/send-attachment`` endpoint.
    )annotationsN)datetimetimezone)Path)TYPE_CHECKINGAnyDictListOptional)urlparseTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageTypeProcessingOutcome
SendResult)compile_mention_patternsstrip_markdown   )load_project_credentials)UnscopedSecretError)
get_secretc                z    	 t          | |          }n$# t          $ r t          j        |           }Y nw xY w||n|S )a  Scope-aware credential read with the default-profile startup fallback.

    Secondary profiles construct their adapters under a profile secret
    scope -- the scope is authoritative and a scoped miss returns ``default``
    (no cross-profile borrow from ``os.environ``, which may hold another
    profile's value). The DEFAULT profile's adapter constructs and sends
    *unscoped* under multiplexing, where a bare ``get_secret`` would raise
    ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that
    profile's own value, so fall back to it. Same pattern as the Slack
    ``SLACK_APP_TOKEN`` read (#59739) and
    ``gateway/platforms/whatsapp_common.py::_get_wsecret``.
    )_scoped_get_secret_UnscopedSecretErrorosgetenv)namedefaultvals      F/home/thesage/.hermes/hermes-agent/plugins/platforms/photon/adapter.py_get_scoped_secretr#   J   sR     w//   ioo/33w.s    44iU"  z	127.0.0.1i@  zphoton-sidecar.jsonreturnr   c                 8    ddl m}   |             dz  t          z  S )Nr   get_hermes_homeruntime)hermes_constantsr'   _RUNTIME_RECORD_NAMEr&   s    r"   _runtime_record_pathr+   y   s-    000000?y(+???    portinttokenstrpidNonec                   ddl }	 t                      }|j                            dd           |                    t          |j                  dd          \  }}	 	 t          j        |d           n# t          $ r Y nw xY wt          j	        |d	d
          5 }t          j        | ||d|           ddd           n# 1 swxY w Y   t          j        ||           dS # t          $ r( 	 t          j        |           n# t          $ r Y nw xY w w xY w# t          $ r&}t                               d|           Y d}~dS d}~ww xY w)z@Atomically persist ``{port, token, pid}`` with owner-only perms.r   NT)parentsexist_okz.photon-sidecar.z.tmp)dirprefixsuffixi  wutf-8encoding)r-   r/   r1   z3[photon] failed to write sidecar runtime record: %s)tempfiler+   parentmkdirmkstempr0   r   chmodOSErrorfdopenjsondumpreplaceBaseExceptionunlink	Exceptionloggerwarning)	r-   r/   r1   r=   pathfdtmpfhes	            r"   _write_runtime_recordrQ      s   OOOQ#%%$666""DK  );F # 
 
C	e$$$$   2sW555 J	4%DDbIIIJ J J J J J J J J J J J J J JJsD!!!!! 	 	 		#   	  Q Q QLaPPPPPPPPPQs   AD A5 4C$ 5
B?C$ BC$ C6C$ CC$ 	C
C$ $
D/DD
DDDDD 
E	#EE	Optional[Dict[str, Any]]c                     	 t          j        t                                          d                    } n# t          t
          f$ r Y d S w xY wt          | t                    r| nd S )Nr:   r;   )rD   loadsr+   	read_textrB   
ValueError
isinstancedictraws    r"   _read_runtime_recordr[      sp    j-//9979KKLLZ    ttS$''133T1s   47 AAc                 n    	 t                                          d           d S # t          $ r Y d S w xY w)NT)
missing_ok)r+   rH   rB    r,   r"   _delete_runtime_recordr_      sL    %%%66666   s   "& 
44r   boolc                j   	 t          |           }n# t          t          f$ r Y dS w xY w|dk    rdS 	 ddlm} t           ||                    S # t          $ r Y nw xY wt          j        dk    rB	 t          j	        |d           n)# t          $ r Y dS t          $ r Y dS t          $ r Y dS w xY wdS dS )z8Best-effort liveness check for the recorded sidecar pid.Fr   )_pid_existsposixT)r.   	TypeErrorrV   gateway.statusrb   r`   rI   r   r   killProcessLookupErrorPermissionErrorrB   )r1   pid_intrb   s      r"   _sidecar_pid_aliverj      s   c((z"   uu!||u......KK(()))   	w'	GGQ! 	 	 	55 	 	 	44 	 	 	55	t 4s;    ''A 
AA2B 
B.	B.!	B.-B.i  i        .@)dir_writableresolve_sidecar_dirzOptional[Path]_SIDECAR_DIR_NPM_ERROR_LOGi,  c                 :    t           t                      a t           S )zBSidecar runtime dir, resolved once on first use (never at import).)rn   rm   r^   r,   r"   _sidecar_dirrq      s     *,,r,   c                 @    t           t           S t                      dz  S )zEPath of the persisted npm-failure log (derived from the sidecar dir).Nz.photon-npm-error.log)ro   rq   r^   r,   r"   _npm_error_logrs      s    !>>333r,   iX  )zinternal sidecar errorzupstream connect errorzupstream unavailablezconnection droppedzreset reason: overflowupstream_overflowupstream_unavailable      >@z.pluginpayloadattachment      @z#(?<![\w@])@?hermes\s+agent\b[,:\-]?z(?<![\w@])@?hermes\b[,:\-]?c                  ,     e Zd ZdZdddd fdZ xZS )PhotonSidecarErrorz=Structured failure returned by the supervised Photon sidecar.sidecar_errorFerror_class	retryablerL   r0   status_coder.   errorr|   r}   r`   r$   r2   c                   || _         || _        || _        || _        || _        t                                          d| d| d| d| d| 
           d S )NzPhoton sidecar z
 returned  (z, retryable=): )rL   r~   r   r|   r}   super__init__)selfrL   r~   r   r|   r}   	__class__s         r"   r   zPhotonSidecarError.__init__!  s     	&
&"?d ? ?k ? ?? ?)2? ?7<? ?	
 	
 	
 	
 	
r,   )rL   r0   r~   r.   r   r0   r|   r0   r}   r`   r$   r2   )__name__
__module____qualname____doc__r   __classcell__r   s   @r"   ry   ry     sU        GG +
 
 
 
 
 
 
 
 
 
 
 
r,   ry   rL   r~   textdatac                   |A	 t          j        |          }n# t          $ r i }Y nw xY wt          |t                    r|ni }t          |                    d          p|d d         pd          }t          |                    d          pd          }t          |                    d                    }|dk    r	t          }d}t          | ||||	          S )
Nr      zsidecar errorr|   rz   r}   target_not_allowedF)rL   r~   r   r|   r}   )
rD   rT   rI   rW   rX   r0   getr`   _TARGET_NOT_ALLOWED_MESSAGEry   )rL   r~   r   r   parsedr   r|   r}   s           r"   _sidecar_error_from_responser   5  s     |	Z%%FF 	 	 	FFF	#FD119vvr!!BT$3$ZB?CCEdhh}--@AAKTXXk**++I*** ,	   s    ((u   shared/free-tier Photon lines cannot initiate outbound sends to new targets — upgrade to a dedicated line or use another delivery channelvaluer    c                T    	 t          |           S # t          t          f$ r |cY S w xY wNr.   rd   rV   r   r    s     r"   _coerce_portr   Z  <    5zzz"       ''c                 N    t                      dz  dz                                  S )a  True when spectrum-ts is present under node_modules/.

    Checks the dependency's own directory, not just node_modules/'s
    existence: npm creates node_modules/ before aborting on ENOSPC, a
    network timeout, or EACCES, so an empty/partial node_modules/ would
    otherwise read as "installed". Shared by check_requirements(),
    _start_sidecar(), and `hermes photon status` so all three agree on
    what "installed" means.
    node_moduleszspectrum-ts)rq   existsr^   r,   r"   sidecar_deps_installedr   a  s$     NN^+m;CCEEEr,   floatc                T    	 t          |           S # t          t          f$ r |cY S w xY wr   )r   rd   rV   r   s     r"   _coerce_floatr   n  s<    U||z"   r   c                T    	 t          |           S # t          t          f$ r |cY S w xY wr   r   r   s     r"   _coerce_intr   u  r   r   valuesc                     | D ]}||c S 	dS )zReturn the first value that is not None.

    Unlike ``a or b``, this preserves an explicit falsy value (e.g. ``0`` or
    ``""``) so config can intentionally set a zero/empty without it silently
    falling through to a later source or a default.
    Nr^   )r   vs     r"   
_first_setr   |  s+       =HHH 4r,   excrG   c                    t          | t          j        t          f          rdS t          r*t	          t
          dd          }|t          | |          rdS dt          |           j                                        v S )z<True when *exc* indicates the request timed out (call hung).TTimeoutExceptionNtimeout)	rW   asyncioTimeoutErrorHTTPX_AVAILABLEgetattrhttpxtyper   lower)r   timeout_excs     r"   _is_timeout_errorr     su    #,l;<< t e%7>>"z#{'C'C"4S		*002222r,   c                 *   t           st                              d           dS t          j        t          j        d          pd          s1t                              dt          j        d          pd           dS t                      st          t          j        d                    rt          t                                rdS d} 	 t                                                      rAt                                          d	
                                          dt                   } n# t           $ r Y nw xY w| r)t                              dt                      |            n't                              dt                                 dS dS )zEReturn True when both Python deps and the Node sidecar are available.u1   photon: httpx not installed — pip install httpxFPHOTON_NODE_BINnodez*photon: node binary '%s' not found on PATHnpmT r:   r;   NuY   photon: spectrum-ts not installed at %s (last npm error: %s) — run: hermes photon setupuD   photon: spectrum-ts not installed at %s — run: hermes photon setup)r   rJ   rK   shutilwhichr   r   r   r`   _dir_writablerq   rs   r   rU   strip_NPM_ERROR_LOG_MAX_CHARSrB   debug)	npm_errors    r"   check_requirementsr     s    JKKKu<	"344>?? 8I'((2F	
 	
 	
 u!## ' U##$$ 	|~~)F)F 	4
 		&&(( l*,,666HHNNPPQjRjQjk	 	 	 	D	 	LLD	    LLV   u4s   A!D/ /
D<;D<c                     t                      dz  } t                      dz  dz  }	 |                                 j        |                                j        k    S # t          $ r Y dS w xY w)a(  True when node_modules exists but is older than the committed lockfile.

    `hermes update` rewrites ``package-lock.json`` when the spectrum-ts pin is
    bumped, but does not reinstall ``node_modules``. npm records the state of
    the last install in ``node_modules/.package-lock.json``; when the top-level
    lockfile is newer than that marker, the install is out of date. This is the
    same signal ``npm ci`` uses. Returns False (do nothing) if either file is
    missing or unreadable, so a first-run or odd filesystem never blocks start.
    zpackage-lock.jsonr   z.package-lock.jsonF)rq   statst_mtimerB   )lockfilemarkers     r"   _sidecar_deps_staler     so     ~~ 33H^^n,/CCF}}'&++--*@@@   uus   3A 
A)(A)c                    t          j        d          } | st                              d           dS ddlm} 	 t          j        | dgt          t                                dddd	d
t           |            	  	        }|j        dk    r_t                              d           t          j        | dgt          t                                dddd	d
t           |            	  	        }n6# t          j        $ r$ t                              dt                     Y dS w xY w|j        dk    r=t                              d|j        p|j        pd                                           dS t                              d           dS )u  Reinstall the sidecar's node_modules from the lockfile (blocking).

    Mirrors ``hermes photon install-sidecar``: ``npm ci`` for an exact,
    reproducible install, falling back to ``npm install`` if the lockfile is
    missing or drifted. Runs the postinstall patch as part of the install.
    Best-effort — a failure here just leaves the (stale) deps in place and the
    normal ``_start_sidecar`` readiness check reports the real error.
    r   z=[photon] cannot reinstall stale sidecar deps: npm not on PATHNr   windows_hide_flagsciTr:   rF   F)cwdcapture_outputr   r<   errorscheckr   creationflagsz?[photon] sidecar `npm ci` failed; falling back to `npm install`installz9[photon] sidecar dependency reinstall timed out after %ssz0[photon] sidecar dependency reinstall failed: %sr   z7[photon] sidecar dependencies reinstalled from lockfile)r   r   rJ   rK   hermes_cli._subprocess_compatr   
subprocessrunr0   rq   _NPM_REINSTALL_TIMEOUT
returncodeTimeoutExpiredr   stderrstdoutr   info)r   r   results      r"   _reinstall_sidecar_depsr     s    ,u

C VWWW A@@@@@ $KLNN##	*,,..
 
 
 !!NNQ    ^i ''#GI.0022  F $ 	 	 	
 	G"	
 	
 	
 		 A>]1fm1r88::	
 	
 	
 	
 	

 	MNNNNNs   B/C* */DDcfgr   c                    | j         pi }|                    d          pt          j        d          }|                    d          pt	          d          }|r|s"t                      \  }}t          |o|          S dS )N
project_idPHOTON_PROJECT_IDproject_secretPHOTON_PROJECT_SECRETT)extrar   r   r   r#   r   r`   )r   r   r   r   	stored_id
stored_secs         r"   validate_configr     s    IOE<((JBI6I,J,JJYY/00_4FG^4_4_N .^ . 8 : :	:I,*---4r,   c                     t          |           S r   )r   r   s    r"   is_connectedr   "  s    3r,   Optional[dict]c                     t                      \  } }| r|sdS | |d}t          j        dd                                          }|r|t          j        dd          d|d<   |S )	zSeed PlatformConfig.extra from env so env-only setups appear in status.

    The special ``home_channel`` key is handled by the core plugin hook and
    becomes a proper ``HomeChannel`` on ``PlatformConfig``.
    N)r   r   PHOTON_HOME_CHANNELr   PHOTON_HOME_CHANNEL_NAMEHome)chat_idr   home_channel)r   r   r   r   )r   r   seedhomes       r"   _env_enablementr   &  s     ":!;!;J > t *nMMD9*B//5577D 
I8&AA 
  
^ Kr,   c                 x    t          j        dd                                                                          dvS )a:  Send agent replies as markdown (spectrum-ts ``markdown()`` builder).

    iMessage renders it natively; other Spectrum platforms degrade to
    readable plain text. On-device rendering can't be unit-tested, so
    ``PHOTON_MARKDOWN=false`` is the kill-switch back to stripped plain
    text without a release.
    PHOTON_MARKDOWNtrue>   0nofalser   r   r   r   r^   r,   r"   _markdown_enabledr   9  s<     9&//5577==?? H  r,   Optional[str]c                   | pd                                 }t          j        d|t          j                  sd S 	 t	          |          }n# t
          $ r Y d S w xY w|j                                        dvs|j        sd S |S )Nr   zhttps?://\S+)flags>   httphttps)	r   re	fullmatch
IGNORECASEr   rV   schemer   netloc)r   	candidater   s      r"   _url_only_candidater
  F  s    ""$$I<"-HHH t)$$   tt}$555V]5ts   A 
AAc                @    t                      sdS t          |           S )a&  Return a URL that should be sent via spectrum-ts ``richlink()``.

    Keep this intentionally narrow: only exact http(s) URL messages become
    rich links. Prose containing URLs and Markdown links stay on the normal
    markdown/text path so Hermes does not drop labels or rewrite intent.
    N)r   r
  )r   s    r"   _richlink_candidater  S  s%      tt$$$r,   contentDict[str, Any]c                   t          |                     d          pd                                          }t          |                     d          pd                                          }t          |                     d          pd                                          }g }|r|                    |           |r||k    r|                    |           |r|                    |           |rd                    |          ndS )Nurlr   titlesummary
z'[Photon rich link received with no URL])r0   r   r   appendjoin)r  r  r  r  partss        r"   _format_richlink_contentr  _  s    
gkk%  &B
'
'
-
-
/
/CG$$*++1133E'++i((.B//5577GE U 7e##W
 S$S499U*SSr,   c                   |                      d          }|dk    r$t          |                      d          pd          S |dk    r$t          |                      d          pd          S |dk    rq|                      d          pg D ]Y}t          |t                    s|                     d          pi }t          |t                    rt	          |          }|r|c S Zd S )	Nr   r   r   richlinkr  groupitemsr  )r   r
  rW   rX   _richlink_url_from_content)r  ctypeitemitem_contentr  s        r"   r  r  m  s    KKE"7;;v#6#6#<"===
"7;;u#5#5#;<<<KK((.B 	 	DdD)) 88I..4"L,-- 0>> JJJ4r,   payloadc                   |                      d          dk    rdS t          |                      d          pd                                          }t          |                      d          pd                                          }|                    t                    p+|                    t                    pt          |v pt          |v }|S )Nr   
attachmentFr   r   id)r   r0   r   endswith#_RICHLINK_PREVIEW_ATTACHMENT_SUFFIX)r   r   attachment_idis_preview_payloads       r"   _is_richlink_preview_attachmentr(    s    {{6l**uw{{6""(b))//11DD))/R006688M9:: 	@!!"EFF	@.$6	@ /-?	  r,   c                \   |                      d          dk    r9t          |                      d          p|                      d          pd          S |                      d          dk    rg }|                      d          pg D ]}t          |t                    r|                     d          nd }t          |t                    rL|                    t          |                     d          p|                     d          pd                     d	                    |          pd
S dS )Nr   r"  r   r#  	(unnamed)r  r  r  z, z(group)z	(unknown))r   r0   rW   rX   r  r  )r  labelsr  r  s       r"   _richlink_preview_labelr,    s"   {{6l**7;;v&&J'++d*;*;J{KKK{{6g%%KK((.B 	 	D2<T42H2HR488I...dL,-- ((00YL4D4DT4J4JYkZZ   yy  -I-;r,   c                P   t          |           rdS |                     d          dk    rdS |                     d          pg }|sdS |D ][}t          |t                    s dS |                    d          pi }t          |t                    s dS t          |          s dS \dS )NTr   r  Fr  r  )r(  r   rW   rX   )r  r  r  r  s       r"   _is_richlink_preview_contentr.    s    &w// t{{6g%%uKK  &BE u  $%% 	55xx	**0b,-- 	55.|<< 	55	4r,   c                      e Zd ZdZeZdZd fdZedd
            Z	ddZ
ddZddddZddZddZddZddZddZddZddZddZdd"Zedd&            Zedd(            Zedd)            Zdd*Zdd+Zdd.Zdd/Zdd0Zdd1Zdd3Zdd4Z dd5Z!	 	 ddd>Z"	 dd fdDZ#	 	 	 dd fdGZ$	 	 	 dddIZ%	 	 	 dddKZ&	 	 	 dddMZ'	 	 	 	 dddPZ(	 	 	 dddRZ)	 dddVZ*	 dddXZ+dddYZ,ddZZ-d[Z.d\Z/dd]Z0 e1j2        d^          Z3e4dd_            Z5dd`Z6ddaZ7ddbZ8ddcZ9ddeZ:ddfZ;	 dddgZ<	 dddhZ=ddjZ>dkZ?dlZ@ddmZAddnZBeddp            ZCeddr            ZD	 	 	 	 dddxZEdd{ZFd|d|d}ddZGddZHd6d6d6ddddZIddZJ xZKS )PhotonAdapterzBidirectional bridge to Photon Spectrum via the Node spectrum-ts sidecar.

    Inbound: consume the sidecar's ``/inbound`` gRPC stream.
    Outbound: loopback POSTs to the sidecar's control channel.
    Fconfigr   c                B   t                                          |t          d                     |j        pi }t	                      \  }}t          j        d          p|                    d          p|pd| _        t          d          p|                    d          p|pd| _
        t          |                    d          pt          j        d          t                    | _        t          | _        t          d	          pt!          j        d
          | _        t'          t          j        dd                                                    dv| _        t          j        d          pt-          j        d          pd| _        t3          t5          |                    d          t          j        d                    d          | _        t3          t5          |                    d          t          j        d                    d          | _        t;          t5          |                    d          t          j        d                    d          | _        | j        dk    | _        tA                      | _!        d | _"        d | _#        d | _$        d | _%        d| _&        d | _'        d| _(        d | _)        d| _*        d| _+        d| _,        d | _-        i | _.        i | _/        i | _0        i | _1        i | _2        i | _3        |                    d          }|t          j        d          }t'          |          4                                                                dv | _5        | 6                    d |v r|d          nt          j        d!                    | _7        d S )"Nphotonr   r   r   r   r   sidecar_portPHOTON_SIDECAR_PORTPHOTON_SIDECAR_TOKEN   PHOTON_SIDECAR_AUTOSTARTr   )r   r   r   r   r   probe_interval_secondsPHOTON_PROBE_INTERVAL_SECONDSg     @probe_timeout_secondsPHOTON_PROBE_TIMEOUT_SECONDSg      $@probe_max_failuresPHOTON_PROBE_MAX_FAILURES   r   Frk           require_mentionPHOTON_REQUIRE_MENTION>   1onyesr   mention_patternsPHOTON_MENTION_PATTERNS)8r   r   r   r   r   r   r   r   _project_idr#   _project_secretr   _DEFAULT_SIDECAR_PORT_sidecar_port_DEFAULT_SIDECAR_BIND_sidecar_bindsecrets	token_hex_sidecar_tokenr0   r   _autostart_sidecarr   r   	_node_binr   r   _probe_interval_probe_timeoutr   _probe_max_failures_probe_enabledr   supports_code_blocks_sidecar_proc_sidecar_supervisor_task_inbound_task_sidecar_health_task_inbound_running_http_client_sidecar_health_interval_watchdog_task_watchdog_running_probe_failures_last_upstream_activity_respawn_lock_seen_messages_sent_message_ids_last_inbound_by_chat_recent_richlinks_by_chat_typing_last_sent_pending_fffcr   rA  _compile_mention_patterns_mention_patterns)r   r1  r   r   r   _require_mentionr   s         r"   r   zPhotonAdapter.__init__  s   (!3!3444"
 !9 : :	:I)** yy&& 	 	 677 yy)** 	 	 *IIn%%I3H)I)I!
 
 3566O':KB:O:O 	 #&I0&99#
 #

%''-#. #455Wf9M9MWQW(  -		233	9::   
  
 ,		122	899  
 
 $/		.//	566  $
 $
  #2Q6 %6$7$7! :>@D%59<@! %;?(,%6:!&  (+$59 13 46 68" <>&35;=
 !99%677#!y)ABB"#344::<<BBDD I
  
 "&!?!?!U** $%%455"
 "
r,   rZ   r   r$   'list[re.Pattern]'c                <    t          | dt          t                    S )aD  Compile group-mention wake words from config/env.

        ``raw`` is a list (config or env JSON), a string (env var: JSON
        list, or comma/newline-separated), or None (use Hermes defaults).
        Mirrors the BlueBubbles implementation so both iMessage channels
        accept the same configuration shapes.
        r3  )
log_prefixdefaultslogger_)r   _DEFAULT_MENTION_PATTERNSrJ   rY   s    r"   rj  z'PhotonAdapter._compile_mention_patternsI  s'     (.	
 
 
 	
r,   r   r0   r`   c                Z    r| j         sdS t          fd| j         D                       S )NFc              3  B   K   | ]}|                               V  d S r   )search).0patternr   s     r"   	<genexpr>zBPhotonAdapter._message_matches_mention_patterns.<locals>.<genexpr>\  s/      NNG7>>$''NNNNNNr,   )rk  any)r   r   s    `r"   !_message_matches_mention_patternsz/PhotonAdapter._message_matches_mention_patternsY  s@     	41 	5NNNNt7MNNNNNNr,   c                   |s|S | j         D ]r}|                    |                                          }|rG|                                |                                d                             d          }|p|c S s|S )zStrip a leading wake word before dispatch.

        Custom mention patterns are regexes, so we only strip a leading
        match to avoid deleting ordinary words later in the prompt.
        Nz ,:-)rk  matchlstripend)r   r   rw  r|  cleaneds        r"   _clean_mention_textz!PhotonAdapter._clean_mention_text^  s      	K- 	' 	'GMM$++--00E '++--		5<<VDD$&&&' r,   )is_reconnectr  c                 K   t           s|                     ddd           dS | j        r| j        s|                     ddd           dS t	          j        dd          }|| _        | j        r~	 |                                  d {V  n|# t          $ rU}|                     d	d
| d           t                       |                                 d {V  d | _        Y d }~dS d }~ww xY wt                              d           d| _        t          j                                        |                                           | _        t          j                                        |                                           | _        t-          j                    | _        | j        rc| j        r\t          j                    | _        d| _        t          j                                        |                                           | _        |                                  t                               d| j!        | j"                   dS )NMISSING_DEPhttpx not installedFr}   MISSING_CREDENTIALSzRPHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET are required. Run: hermes photon setuprv   r   	trust_envSIDECAR_FAILEDz failed to start Photon sidecar: TuD   [photon] sidecar autostart disabled — inbound + outbound will failuD   [photon] connected — sidecar on %s:%d, streaming inbound over gRPC)#r   _set_fatal_errorrH  rI  r   AsyncClientr]  rQ  _start_sidecarrI   r_   acloserJ   rK   r\  r   get_event_loopcreate_task_inbound_looprZ  _monitor_sidecar_healthr[  time	monotonicrb  rV  Lockrc  r`  _presence_watchdogr_  _mark_connectedr   rM  rK  )r   r  clientrP   s       r"   connectzPhotonAdapter.connecto  s      	!!4 "    5 	t'; 	!!%+	 "    5"45AAA" " 	))++++++++++   %%$:q::" &    '(((mmoo%%%%%%%$(!uuuuu NNV  
 !%$355AA  
 
 %,$:$<$<$H$H((**%
 %
! (,~'7'7$ 	4#: 	!(D%)D"")"8":":"F"F''))# #D 	R 2	
 	
 	
 ts   1B 
C+A
C&&C+r2   c                  K   d| _         |                                  d {V  | j        a| j        }d | _        |                                 |t	          j                    ur*	 | d {V  n # t          j        $ r Y nt          $ r Y nw xY w| j        a| j        }d | _        |                                 |t	          j                    ur*	 | d {V  n # t          j        $ r Y nt          $ r Y nw xY wt          | j
                                                  D ]2\  }\  }}|r(|                                s|                                 3| j
                                         |                                  d {V  | j        8	 | j                                         d {V  n# t          $ r Y nw xY wd | _        |                                  d S NF)r\  _stop_watchdogr[  cancelr   current_taskCancelledErrorrI   rZ  listri  r  doneclear_stop_sidecarr]  r  _mark_disconnected)r   taskchat_key_	fffc_tasks        r"   
disconnectzPhotonAdapter.disconnect  sZ      % !!#########$0,D(,D%KKMMM7/1111JJJJJJJJ-   D    D)%D!%DKKMMM7/1111JJJJJJJJ-   D    D )-T-?-E-E-G-G(H(H 	# 	#$Hnq) #!1!1 #  """  """  """""""""('..0000000000    $D!!!!!sB   #A, ,B	=	B	B	C C1%	C10C1F( (
F54F5c                R    t          j        |                                            dS )a  Notify the gateway of a fatal error from a detached task.

        ``_monitor_sidecar_health`` and ``_supervise_sidecar`` run as
        ``self._sidecar_health_task`` / ``self._sidecar_supervisor_task`` and
        used to call ``await self._notify_fatal_error()`` directly, on their
        own call stack. That routes straight into
        ``GatewayRunner._handle_adapter_fatal_error_impl``, which tears the
        adapter down via ``_safe_adapter_disconnect`` -> ``disconnect()``.
        ``disconnect()`` cancels ``self._sidecar_health_task`` and awaits it
        to make sure it's really gone -- but when the *health task itself* is
        what's driving this whole notification (the common case: the health
        poll is what detected the fatal condition), that task IS the one
        awaiting several frames up the stack, so ``disconnect()`` cancels and
        awaits its own caller. ``disconnect()``'s
        ``task is not asyncio.current_task()`` guard doesn't catch this,
        because the wrapper task ``_await_adapter_cleanup_with_timeout``
        creates around ``disconnect()`` (via ``asyncio.ensure_future``) is
        the current task there, not the health/supervisor task further up
        the plain-await chain. The self-cancellation throws an unhandled
        ``CancelledError`` through the health/supervisor task with nothing
        to catch it -- ``CancelledError`` stopped subclassing ``Exception``
        in Python 3.8, so the ``except Exception`` around the notify call
        never saw it -- and the task dies silently mid-handoff: no log line,
        no retry, the platform stranded with no automated way back.

        Dispatching the notification onto a brand-new task instead -- the
        same pattern ``DiscordAdapter._handle_bot_task_done`` already uses --
        means ``disconnect()`` can cancel the health/supervisor task freely
        without that cancellation ever reaching back into the code that's
        still running the handoff, so the handoff always reaches the
        reconnect queue.
        N)r   r  _notify_fatal_error_loggedr   s    r"   _dispatch_fatal_notificationz*PhotonAdapter._dispatch_fatal_notification  s(    B 	D;;==>>>>>r,   c                   K   	 |                                   d {V  d S # t          $ r&}t                              d|           Y d }~d S d }~ww xY w)Nz,[photon] fatal-error notification failed: %s)_notify_fatal_errorrI   rJ   rK   )r   r   s     r"   r  z(PhotonAdapter._notify_fatal_error_logged  s      	P**,,,,,,,,,,, 	P 	P 	PNNI3OOOOOOOOO	Ps     
AAAc                  K   | j         }|dS d| j         d| j         d}d| j        i}d}| j        rI	 |                    d||d          4 d{V 	 }|j        d	k    rt          d
|j                   d}|                                2 3 d{V }| j        s n4|	                                }|s'| 
                    |           d{V  C6 	 ddd          d{V  n# 1 d{V swxY w Y   n|# t          j        $ r  t          $ r`}| j        sY d}~dS t                              d||           t          j        |           d{V  t#          |dz  d          }Y d}~nd}~ww xY w| j        GdS dS )zConsume the sidecar's ``/inbound`` NDJSON stream, with reconnect.

        The sidecar owns the gRPC reconnect/heartbeat to Photon; this loop
        only has to re-open the loopback HTTP stream if it drops (e.g. the
        sidecar restarts).
        Nhttp://:z/inboundX-Hermes-Sidecar-Tokeng      ?GETheadersr   r   z/inbound returned z;[photon] inbound stream dropped (%s); reconnecting in %.1fs   rv   )r]  rM  rK  rP  r\  streamr~   RuntimeErroraiter_linesr   _on_inbound_liner   r  rI   rJ   rK   sleepmin)r   r  r  r  backoffresplinerP   s           r"   r  zPhotonAdapter._inbound_loop  s      ">FI*IIT-?III+T-@A# 	11!==3 )   : : : : : : : :'3..*+R@P+R+RSSS!G&*&6&6&8&8 : : : : : : :d#4 "!E#zz||# %$"33D9999999999 '9&8: : : : : : : : : : : : : : : : : : : : : : : : : : : )    1 1 1, EEEEEQw   mG,,,,,,,,,gk4001# # 	1 	1 	1 	1 	1sN   C7 9C%C>C%C7 %
C//C7 2C/3C7 7E0E+A	E++E0c                F  K   | j         rt          j        | j                   d{V  | j         sdS 	 |                     di            d{V }nA# t          j        $ r  t          $ r%}t                              d|           Y d}~d}~ww xY wt          |t                    r|                    d          nd}t          |t                    s|                    d          }t          |t                    rY|                    d          du rBt                              d|                    d	          |                    d
                     |                    d          durmt          |                    d          pd          }|                    d          }t          |                    d          pd          }d| d| d| }t                              d|           |                     d|d           |                                  dS dS )a  Promote degraded upstream Photon stream health into reconnect.

        The sidecar HTTP process can stay alive while spectrum-ts repeatedly
        fails to maintain the upstream inbound gRPC stream. Polling `/healthz`
        keeps that from becoming a silent inbound outage.
        N/healthzz([photon] sidecar health check failed: %sr  	stalenesszombieSuspectedTzV[photon] sidecar reports suspected zombie stream (silentForMs=%s, lastProbeOutcome=%s)silentForMslastProbeOutcomeokFstateunknowndegradedForMs	lastIssuezunknown stream issuez'Photon upstream stream degraded (state=z, degradedForMs=r   [photon] %sUPSTREAM_STREAM_DEGRADEDr  )r\  r   r  r^  _sidecar_callr  rI   rJ   r   rW   rX   r   rK   r0   r   r  r  )	r   r   r   r  r  r  degraded_for_ms
last_issuemessages	            r"   r  z%PhotonAdapter._monitor_sidecar_health-  s{      # 2	- =>>>>>>>>>( !//
B????????)      GMMM ,6dD+A+AKTXXh'''tFfd++  

;//I9d++	MM"344<<=MM-00MM"455	   zz$u,,

7++8y99E$jj99OVZZ44N8NOOJ     2A       
 LL000!!* "   
 --///Ee2	 2	s   A B*B

Br  c                  K   |                                   	 t          j        |          }n0# t          j        $ r t                              d           Y d S w xY w|                    d          }|r|                     |          rd S 	 |                     |           d {V  d S # t          $ r t          
                    d           Y d S w xY w)Nz'[photon] skipping non-JSON inbound line	messageIdz [photon] inbound dispatch failed)_note_upstream_activityrD   rT   JSONDecodeErrorrJ   r   r   _is_duplicate_dispatch_inboundrI   	exception)r   r  eventmsg_ids       r"   r  zPhotonAdapter._on_inbound_lineh  s      	$$&&&	Jt$$EE# 	 	 	LLBCCCFF	 ;'' 	d((00 	F	A((/////////// 	A 	A 	A?@@@@@@	As!   - )AAB) )$CCr  c                X   t          j                     }| j        }|                    |          }|||z
  t          k     rdS ||v r||= |||<   t	          |          t
          k    rDt          |                                          d t	          |          t
          z
           D ]}||= dS )NTF)r  rd  r   _DEDUP_WINDOW_SECONDSlen_DEDUP_MAX_SIZEr  keys)r   r  nowseentolds         r"   r  zPhotonAdapter._is_duplicate|  s    ikk"HHV=S1W'<<<4 T>>VVt99&&DIIKK(()F3t99+F)FG  IIur,   r  
message_idc                   K   t          j        t                     d {V  | j                            |d           rt
                              d||           d S d S )Nz_[photon] wait for attachment was too long, can't retrieve attachment data (message %s, chat %s))r   r  _FFFC_WAIT_SECONDSri  poprJ   rK   )r   r  r  s      r"   _fffc_timeout_handlerz#PhotonAdapter._fffc_timeout_handler  sx      m./////////!!(D11 	NN()3X    	 	r,   r  r  c                  K   |                     d          pi }|                     d          pi }|                     d          pi }|                     d          pd}|st                              d           dS |                     d          d	k    rd	nd
}|                     d          p|                     d          p|}|                     d          pd}	 |r(t          j        |                    dd                    nt          j        t          j                  }	n/# t          $ r" t          j        t          j                  }	Y nw xY wg }
g }d9d}|                     d          }|dk    r|                     d          }|                     d          dk    p
|o|| j
        v }|st                              d           dS |                     d          pd}|                     |||||pd          }|                     t          d| t          j        ||                     d          ||                     d          pdd||	 	  	                   d{V  dS |d!k    r|                     d!          pd                                d"k    r|}| j                            |d          }|r<|d#         r4|d#                                         s|d#                                          t-          j        |                     ||                     d          pd                    }t3          j                    |f| j        |<   t                              d$           dS |d%v rs| j                            |d          }|rV|d#         rN|d#                                         s4|d#                                          t                              d&           |                     ||          r*t                              d't;          |                     dS |                     ||                     d                     |d(k    r|                     d)          d*u rt                              d+           dS |                     d,          pd                                }|st                              d-           dS |                     |||||pd          }|                     t          |t          j        ||                     d          ||	.                     d{V  dS |d!k    r%|                     d!          pd}t          j        }n|d%v r ||          \  }}}
}n|d/k    rt?          |          }t          j        }n|d	k    rg }t          j        }|                     d0          pg D ]=}tA          |tB                    s|                     d          pi }tA          |tB                    sF|                     d          }|d!k    r/|                     d!          pd}|r|"                    |           |d/k    r#|"                    t?          |                     |d%v re ||          \  }}}} |t          j        k    r|}|
#                    |           |#                    |            |s|"                    |           "|r|"                    d1| d2           ?|
r|t          j        k    rt          j$        }d3%                    d4 |D                                                       }|s|
rd5nd6}nd1| d2}t          j        }|d	k    rM| j&        rF| '                    |          st                              d7           dS | (                    |          }| )                    |tU          |          p|           |                     |||||pd          }t          ||||                     d          ||	|
|8          }!|                     |!           d{V  dS ):uq  Normalize a sidecar inbound event and dispatch it to the gateway.

        Event shape (from ``sidecar/index.mjs``)::

            {
              "messageId": "...",
              "platform": "iMessage",
              "space": {"id": "...", "type": "dm"|"group", "phone": "+E164"},
              "sender": {"id": "+E164"},
              "content": {"type": "text", "text": "..."}
                       | {"type": "attachment"|"voice", "id", "name",
                          "mimeType", "size", "duration"?, "data"?,
                          "encoding"?}
                       | {"type": "reaction", "emoji": "❤️",
                          "targetMessageId": "..." | null,
                          "targetDirection": "inbound"|"outbound" | null,
                          "targetText": "..." | null},
              "timestamp": "2026-05-14T19:06:32.000Z"

        Attachment and voice content carry the bytes inline as base64 ``data``
        (with ``encoding == "base64"``) when the sidecar could read them
        within its size cap; otherwise only metadata is present and we surface
        a marker.
            }
        spacesenderr  r#  r   z![photon] inbound missing space.idNr   r  dmphone	timestampZz+00:00)tzr   r  r$   -tuple[str, MessageType, List[str], List[str]]c                (   |                      d          dk    }|                      d          p|rdnd}|                      d          pd}|s/|                                                    d          s|dk    rd	}|rt          j        nt          |          }t          | |||
          }|r|rdnd||g|p|rdndgfS |rdnd}|                      d          }t          |t          t          f          rd| dnd}d| d| d|pd | d|g g fS )Nr   voicer   r*  mimeTyper   .cafaudio/x-cafTforce_audioz(voice)(attachment)	audio/mp4zapplication/octet-streamr"  durationz, duration: sz[Photon z received: r   zunknown MIMEz)])
r   r   r$  r   VOICE_attachment_message_type_cache_inbound_attachmentrW   r.   r   )	r   is_voicer   mimemtypecachedlabelr  duration_texts	            r"   _normalize_binary_payloadzBPhotonAdapter._dispatch_inbound.<locals>._normalize_binary_payload  s    {{6**g5H;;v&&Ph+O77KD;;z**0bD   !6!6v!>!>  $-BWBW)1UK%%7OPT7U7UE.t  F  !)=II~HVXUkk;UW	   (9GG\E{{:..H he55*x**** >5 > >T > >*N>,9> > > r,   reactiontargetMessageIdtargetDirectionoutboundz6[photon] ignoring reaction on a message we didn't sendemoji)r   	chat_name	chat_typeuser_id	user_namezreaction:added:r  
targetTextT)	r   message_typesourcer  reply_to_message_idreply_to_textreply_to_is_own_messageraw_messager  r   u   ￼r   u?   [photon] U+FFFC placeholder received — waiting for attachment>   r  r"  u9   [photon] attachment arrived — cancelling U+FFFC timeoutz5[photon] suppressing rich-link preview attachment: %spoll_optionselectedFz"[photon] ignoring poll deselectionr  z,[photon] ignoring poll vote with empty title)r   r  r  r  r  r  r  r  z"[Photon content type not handled: ]r  c              3     K   | ]}||V  	d S r   r^   )rv  parts     r"   rx  z2PhotonAdapter._dispatch_inbound.<locals>.<genexpr>  s'      AAdDATAAAAAAr,   r  z[Photon empty group received]zR[photon] ignoring group message (require_mention=true, no mention pattern matched))r   r  r  r  r  r  
media_urlsmedia_types)r   r  r$   r  )+r   rJ   rK   r   fromisoformatrF   r  r   utcrV   re  r   build_sourcehandle_messager   r   TEXTr   ri  r  r  r  r   r  r  r  r  _is_recent_richlink_previewr   r,  _record_last_inboundr  rW   rX   r  extendDOCUMENTr  rA  rz  r  _record_recent_richlinkr  )"r   r  r  r  r  space_idr  	sender_idts_strr  r  r  r  r  	target_idis_oursr
  r  r  prevr  choicer   r  
text_partsr  r  	item_type	item_textr   
item_mtype	item_urls
item_typesmessage_events"                                     r"   r  zPhotonAdapter._dispatch_inbound  s	     4 		'""(b8$$*))I&&,"99T??(b 	NN>???F  %yy00G;;GG	JJt$$F		'(:(:Fh	;''-2	6 3&v~~c8'D'DEEE\X\222 I
  	6 	6 	6 555III	6
 !#
!##	 #	 #	 #	J F##J
  $566Ikk"344
B Ai4+AA   L   KK((.BE&& "#!#+t '  F %%2522!,!1!$yy55(1")++l";";"Ct,0 %'
 
 
         F F??F 3 3 9r@@BBhNNH%))(D99D !Q !Q !Q   &**8UYY{5K5K5QrRR D -1N,<,<d+CDx(LLZ[[[F +++%))(D99D ZQ ZQ ZQ   XYYY
 ++Hg>> 	KKG'00   F
 	!!(EIIk,B,BCCCM!! {{:&&%//ABBBkk'**0b7799F KLLL&& "#!#+t '  F %%!,!1!$yy55 %'  	 	 	 	 	 	 	 	 	 FF??;;v&&,"D$EE---3L3LW3U3U0D%[[j  +G44D$EEg$&J$EG,,2 Y Y!$-- #xx	228b!,55 (,,V44	&& , 0 0 8 8 >BI  5")))444
**%%&>|&L&LMMM 777@Y@Y$A A=FJ	:  000 *%%i000&&z222$ 2"))&111 Y%%&W9&W&W&WXXX -e{'777#,99AAjAAAAAGGIID Y)3X~~9X@@@@D$E D$899$?? I   ++D11D$$X/I'/R/R/ZVZ[[[""'4 # 
 
 %yy--!#	
 	
 	
 !!-00000000000s   A	D( ()EEr-   r.   	List[int]c           	         	 t          j        ddd|  dgdddddd	
          }n# t          t           j        f$ r g cY S w xY wd |j                                        D             S )zBPIDs listening on a local TCP port (empty if none/undeterminable).lsofz-tiztcp:z-sTCP:LISTENTr:   rF   rw   Fr   r   r<   r   r   r   c                x    g | ]7}|                                                                 (t          |          8S r^   )r   isdigitr.   )rv  toks     r"   
<listcomp>z5PhotonAdapter._find_listener_pids.<locals>.<listcomp>  s7    PPPS#))++:M:M:O:OPCPPPr,   )r   r   rB   r   r   split)r-   outs     r"   _find_listener_pidsz!PhotonAdapter._find_listener_pids  s    	.t~>#$\_gl  CC 23 	 	 	III	PPCJ$4$4$6$6PPPPs   "% A A r1   c           	         	 t          j        ddt          |           ddgdddddd	
          }n# t          t           j        f$ r Y d	S w xY wd|j        v S )z;True if ``pid``'s command line is a Photon sidecar process.psz-pz-ozcommand=Tr:   rF   rw   Fr8  zphoton/sidecar/index.mjs)r   r   r0   rB   r   r   )r1   r>  s     r"   _pid_is_sidecarzPhotonAdapter._pid_is_sidecar  s}    	.tSXXtZ8#$\_gl  CC 23 	 	 	55	 *SZ77s   -0 A
	A
c                T    	 t          j        | d           dS # t          $ r Y dS w xY w)Nr   TF)r   rf   rB   )r1   s    r"   
_pid_alivezPhotonAdapter._pid_alive  s=    	GCOOO4 	 	 	55	s    
''c                   K   t           j        dk    rdS 	 t          j        dd          4 d{V }|                    d j         d j         dd	 j        i
           d{V  ddd          d{V  n# 1 d{V swxY w Y   n# t          j        $ r Y dS w xY w fd}t          j
        |           d{V \  }}|st          d j         d|pd d          |D ]S}t                              d| j                   	 t          j        |t           j                   D# t$          $ r Y Pw xY wt'          j                    dz   }t'          j                    |k     rgt)           fd|D                       rLt          j        d           d{V  t'          j                    |k     rt)           fd|D                       L|D ]H}                     |          r1	 t          j        |t           j                   8# t$          $ r Y Dw xY wIt          j        d           d{V  |rt          d j         d| d          dS )u:  Kill an orphaned sidecar squatting our port before spawning ours.

        A hard gateway exit (crash, SIGKILL, supervisor restart) used to leave
        the detached sidecar running with a token the new gateway doesn't
        know, so it can't be told to ``/shutdown`` — and every replacement
        spawn died on EADDRINUSE, failing each reconnect attempt. The
        stdin-EOF watch prevents new orphans; this reclaims the port from
        orphans that predate it (or survived it). Listeners are verified by
        command line before being signalled.
        win32N       @Fr  r  r  r  r  r  c                 v                         j                  } fd| D             fd| D             fS )Nc                >    g | ]}                     |          |S r^   )rB  )rv  r1   r   s     r"   r<  zGPhotonAdapter._reap_stale_sidecar.<locals>._inspect.<locals>.<listcomp>  s,    FFFCD,@,@,E,EFCFFFr,   c                    g | ]}|v|	S r^   r^   )rv  r1   mines     r"   r<  zGPhotonAdapter._reap_stale_sidecar.<locals>._inspect.<locals>.<listcomp>  s    BBB##T//#///r,   )r?  rK  )foundrL  r   s    @r"   _inspectz3PhotonAdapter._reap_stale_sidecar.<locals>._inspect  sS    ,,T-?@@EFFFF5FFFDBBBBBBBBBr,   zport z% is in use by another process (pids: r  uR   , not a Photon sidecar) — free it or set PHOTON_SIDECAR_PORT to a different portz5[photon] reaping orphaned sidecar (pid %d) on port %d      @c              3  B   K   | ]}                     |          V  d S r   )rD  )rv  pr   s     r"   rx  z4PhotonAdapter._reap_stale_sidecar.<locals>.<genexpr>  s/      ,O,OAT__Q-?-?,O,O,O,O,O,Or,   g?皙?z. is also held by non-sidecar processes (pids: u<   ) — free it or set PHOTON_SIDECAR_PORT to a different port)sysplatformr   r  postrM  rK  rP  RequestErrorr   	to_threadr  rJ   rK   r   rf   signalSIGTERMrB   r  ry  r  rD  SIGKILL)r   r  rN  staleforeignr1   deadlines   `      r"   _reap_stale_sidecarz!PhotonAdapter._reap_stale_sidecar  s      <7""F	(FFF       &kkOd0OO43EOOO5t7JK "                                   
 ! 	 	 	FF		C 	C 	C 	C 	C
  '0::::::::w 	J* J J!.YJ J J  
  	 	CNNGT'  V^,,,,   9;;$ikkH$$,O,O,O,O,O,O,O)O)O$-$$$$$$$$$ ikkH$$,O,O,O,O,O,O,O)O)O$ 	 	Cs## GC0000   D mC          	;* ; ;$+; ; ;  	 	sX   B 6A;)B ;
BB B	B B B 
D**
D76D7(H
HHc                	  K   t                      st                              dt                                 t	          j        t                     d {V  t                      s.t          dt                       dt                       d          t                      r9t          	                    d           t	          j        t                     d {V  | 
                                 d {V  t          j                                        }| j        |d<   | j        |d<   t!          | j                  |d<   | j        |d	<   | j        |d
<   d|d<   ddlm} 	 t	          j        t,          j        | j        t!          t                      dz            t!          t                                gdddddd |            	  	         d {V }|j        dk    r/t          |j        p|j        pd                                          |j                                        r2t                              d|j                                                   n2# t<          $ r%}t          	                    d|           Y d }~nd }~ww xY wt-          j        | j        t!          t                      dz            gt,          j         t,          j         t,          j!        |tD          j#        dk     |                      | _$        t	          j%                    }|&                    | '                    | j$                            | _(        tS          j)                    dz   }d }tU          j+        dd          4 d {V }tS          j)                    |k     r| j$        ,                                +t[                       t          d| j$        j         d           	 |.                    d!| j         d"| j         d#d$| j        i%           d {V }	|	j/        d&k    r9ta          | j        | j        | j$        j1                   	 d d d           d {V  d S n# tT          j2        $ r}
|
}Y d }
~
nd }
~
ww xY wt	          j3        d'           d {V  tS          j)                    |k     	 d d d           d {V  n# 1 d {V swxY w Y   t[                       t          d(|           ))Nz7[photon] sidecar deps not installed; installing into %sz0Photon sidecar deps could not be installed into z& (see log for the npm error). Run: cd z' && npm ci   (or `hermes photon setup`)zX[photon] sidecar deps are stale (lockfile newer than install); reinstalling before startr   r   r5  PHOTON_SIDECAR_BINDr6  rC  PHOTON_SIDECAR_WATCH_STDINr   r   z$patch-spectrum-mixed-attachments.mjsTr:   rF   
   F)r   r   r<   r   r   r   r   r   r  z<[photon] failed to apply Spectrum mixed attachment patch: %sz	index.mjsrF  )stdinr   r   envstart_new_sessionr   rk   rG  r  z Photon sidecar exited with code z before becoming readyr  r  r  r  rH  r   rR  z0Photon sidecar did not become ready within 15s: )4r   rJ   r   rq   r   rW  r   r  r   rK   r^  r   environcopyrH  rI  r0   rK  rM  rP  r   r   r   r   rR  r   r   r   r   r   rI   PopenPIPESTDOUTrS  rT  rX  r  r  _supervise_sidecarrY  r  r   r  pollr_   rU  r~   rQ   r1   rV  r  )r   rd  r   patchr   loopr]  last_errr  r  rP   s              r"   r  zPhotonAdapter._start_sidecar  sV     %'' 	 KKI   #$;<<<<<<<<<)++ "W#~~W W+~~W W W      	=NN,   #$;<<<<<<<<<&&(((((((((joo#'#3 '+';#$%();%<%<!"%)%7!"&*&9"# -0() 	EDDDDD	 "+N)OOPP''
  $GI 1022        E 1$$"EL$FEL$FB#M#M#O#OPPP|!!## B]EL,>,>,@,@AAA 	 	 	NNN       	 (-^S+!=>>?/?$"|w6 -,..
 
 
 %''(,(8(8##D$677)
 )
%
 9;;%(,$SEBBB 	) 	) 	) 	) 	) 	) 	)f)++((%**,,8*,,,&Q-8Q Q Q  !!'S$"4SSt7ISSS!94;N O "- " "      D '3.. . . / .2  
 -	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) / ) ! ! ! HHHHHH!mC(((((((((1 )++(((	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)4 	   IxII
 
 	
sW   C7I 
JI??J7AR2A%QR2Q*Q% R2%Q**5R22
R<?R<procsubprocess.Popenc                V  K   |j         dS |j         }t          j                    }	 	 |                    d|j                   d{V }|snBt
                              d|                    dd                                                     fn2# t          $ r%}t
          
                    d|           Y d}~nd}~ww xY w| j        ra|                                }t
                              d|           |                     dd	| d
d           |                                  dS dS )z1Pump the sidecar's stdout/stderr into our logger.NTz[photon-sidecar] %sr:   rF   z&[photon-sidecar] supervisor exited: %suG   [photon] sidecar exited unexpectedly (code %s) — triggering reconnectSIDECAR_CRASHEDz)Photon sidecar exited unexpectedly (code )r  )r   r   r  run_in_executorreadlinerJ   r   decoderstriprI   rK   r\  rl  r   r  r  )r   rp  r   rn  r  rP   	exit_codes          r"   rk  z PhotonAdapter._supervise_sidecar  st     ;F%''	H]!11$HHHHHHHH 14;;w	3R3R3Y3Y3[3[\\\	]  	H 	H 	HNNCQGGGGGGGG	H  	0		ILLY   !!!HIHHH "   
 --/////	0 	0s   A'B 
B>B99B>c                  K   | j         }|t                       d S 	 |j        +	 |j                                         n# t          $ r Y nw xY w| j        M	 | j                            d| j         d| j         dd| j	        id           d {V  n# t          $ r Y nw xY w	 |
                    d           n# t          j        $ r t          j        d	k    rc	 t          j        t          j        |j                  t&          j                   n?# t*          t,          f$ r |                                 Y nw xY w|                                 	 |
                    d           n)# t          j        $ r |                                 Y nw xY wY nw xY wd | _         t                       | j        >t5          j                    }| j        |ur| j                                         d | _        d S d S # d | _         t                       | j        <t5          j                    }| j        |ur| j                                         d | _        w xY w)
Nr  r  z	/shutdownr  rG  r  rO  )r   rF  )rX  r_   rc  closerI   r]  rU  rM  rK  rP  waitr   r   rS  rT  r   killpggetpgidr1   rX  rY  rg   rh   	terminaterf   rY  r   r  r  )r   rp  r  s      r"   r  zPhotonAdapter._stop_sidecar  s     !< #$$$F@	5 z%J$$&&&&    D  ,+00T$"4TTt7ITTT!94;N O # 1          
 !   D 		#	&&&&,      <7**)	"*TX"6"6GGGG.@ ) ) )((((() NN$$$ IIcI****!0      IIKKKKK   "&D #$$$,8.  '3550DD188:::04---5 98	 "&D #$$$,8.  '3550DD188:::04-4444s   G4 ? G4 
A	G4 A
G4 ;B G4 
B G4 B  G4 $B; :G4 ;F6DF%D:7F9D::FE)(F)#FFFFG4 FG4 4AIc                D    t          j                    | _        d| _        dS )zRecord proof the upstream gRPC channel is live, and clear failures.

        Called on every inbound line and after every successful probe.
        r   N)r  r  rb  ra  r  s    r"   r  z%PhotonAdapter._note_upstream_activity  s"    
 (,~'7'7$ r,   c                  K   | j         }|dS d| j         d| j         d}	 |                    |d| j        i| j                   d{V }nr# t          j        $ r  t          $ rV}t          |          r!t                              d|           Y d}~d	S t                              d
|           Y d}~dS d}~ww xY w|j        dk    rdndS )u  Drive one liveness probe via the sidecar's ``/probe``.

        Returns a strict tri-state verdict:

        - ``"alive"``        — the sidecar completed a real upstream gRPC
          round-trip (HTTP 200). Only this counts as proof of liveness.
        - ``"hung"``         — the probe HTTP call itself timed out: the
          sidecar's event loop is unresponsive. Counts toward respawn.
        - ``"inconclusive"`` — anything else (503 from the sidecar's strict
          probe, connection refused, transport error). NEVER counts as alive
          and NEVER counts toward a respawn: a rejected upstream probe may
          just mean the network is down, and a dead sidecar process is the
          supervisor's job, not ours.
        Ninconclusiver  r  z/prober  r  z![photon] probe HTTP call hung: %shungz1[photon] probe transport error (inconclusive): %sr   alive)r]  rM  rK  rU  rP  rT  r   r  rI   r   rJ   r   r~   )r   r  r  r  rP   s        r"   _probe_oncezPhotonAdapter._probe_once  s/      ">!>G*GGT-?GGG	"143FG+ %        DD
 % 	 	 	 	" 	" 	" ## @!DDDvvvvvLLLaPPP!>>>>>	" *c11ww~Es   *A B<&*B7B77B<reasonc                  K   | j         }|t          j                    x}| _         |                                rt                              d           dS |4 d{V  t                              d|           	 |                                  d{V  n*# t          $ r t          	                    d           Y nw xY w	 | 
                                 d{V  n<# t          $ r/ t          	                    d           Y ddd          d{V  dS w xY w|                                  t                              d           ddd          d{V  dS # 1 d{V swxY w Y   dS )a  Tear down and restart the sidecar to recover a dead gRPC stream.

        A fresh ``Spectrum()`` re-subscribes the inbound stream and
        re-registers presence with Photon cloud. The inbound loop's existing
        reconnect logic re-opens the loopback NDJSON stream automatically once
        the new sidecar's ``/inbound`` is up. Guarded by a lock so overlapping
        triggers (watchdog + a manual call) can't double-spawn.
        Nz.[photon] respawn already in progress; skippingu5   [photon] presence watchdog: %s — respawning sidecarz.[photon] error stopping sidecar during respawnz7[photon] failed to respawn sidecar; watchdog will retryzB[photon] presence watchdog: sidecar respawned, gRPC stream renewed)rc  r   r  lockedrJ   r   rK   r  rI   r  r  r  )r   r  locks      r"   _respawn_sidecarzPhotonAdapter._respawn_sidecar3  s      !<(/6D4%;;== 	KKHIIIF 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^NNG  S((********** S S S  !QRRRRRS))++++++++++     M   	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ ((***KK\]]]%	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^sZ   E;BE$B=:E<B==ECE$D ED1E
E#&E#c                0  K   t          j        | j                   d{V  | j        rl	 t	          j                    | j        z
  }|| j        k     r#t          j        | j        |z
             d{V  R|                                  d{V }|dk    r|                                  n|dk    rj| xj	        dz  c_	        t                              d| j	        | j                   | j	        | j        k    r#|                     | j	         d           d{V  nt                              d           n9# t           j        $ r  t           $ r t                              d           Y nw xY wt          j        | j                   d{V  | j        jdS dS )	u!  Periodically confirm the sidecar (and its stream) is responsive.

        The sidecar owns primary zombie-stream detection (silence tracking +
        upstream probe -> degraded /healthz -> exit 75). This loop is the
        adapter's conservative second layer: it probes on a long interval,
        skips the probe when natural inbound traffic already proved liveness,
        and only counts a *hung* probe (the sidecar HTTP call itself timing
        out) toward a respawn. Inconclusive probes — the sidecar answered but
        couldn't prove upstream liveness — reset nothing and trigger nothing:
        the network may just be down, and restarting can't fix that. After
        ``_probe_max_failures`` consecutive hung probes we respawn the sidecar
        to force a fresh stream.
        Nr  r  r   z$[photon] presence probe hung (%d/%d)z consecutive hung probesz6[photon] presence probe inconclusive; taking no actionz+[photon] presence watchdog iteration failed)r   r  rS  r`  r  r  rb  r  r  ra  rJ   rK   rU  r  r   r  rI   r  )r   idleverdicts      r"   r  z PhotonAdapter._presence_watchdogV  s(       mD0111111111$ "	6 P ~''$*FF$...!-(<t(CDDDDDDDDD $ 0 0 2 2222222g%%002222&& ((A-((NN>,d.F   +t/GGG"33#3MMM         LLP   )    P P P  !NOOOOOP- 4555555555E $ "	6 "	6 "	6 "	6 "	6s   AD4 4B?D4 43E*)E*c                   K   d| _         | j        Q| j                                         	 | j         d {V  n # t          j        $ r Y nt
          $ r Y nw xY wd | _        d S d S r  )r`  r_  r  r   r  rI   r  s    r"   r  zPhotonAdapter._stop_watchdog  s      !&*&&((())))))))))      "&D +*s   9 A
	AANr   r  reply_tor   metadatarR   r   c                d   K   |                      ||                     |                     d {V S r   )_sidecar_sendformat_message)r   r   r  r  r  s        r"   sendzPhotonAdapter.send  s>       ''1D1DW1M1MNNNNNNNNNr,   questionchoicesOptional[list]
clarify_idsession_keyc                  K   |s,t                                          ||||||           d {V S ddlm}  ||           |                     ||t          |                     d {V }|j        sLt                              d|j	                   t                                          ||||||           d {V S |S )Nr   )mark_awaiting_textz<[photon] poll clarify failed (%s); falling back to text list)
r   send_clarifytools.clarify_gatewayr  _sidecar_send_pollr  successrJ   rK   r   )
r   r   r  r  r  r  r  r  r   r   s
            r"   r  zPhotonAdapter.send_clarify  s(       	 --7JX        
 	=<<<<<:&&&..w$w--PPPPPPPP~ 	
 NNN   --7JX         r,   	image_urlcaptionc                   K   	 ddl m}  ||           d {V }n:# t          $ r- t                                          ||||           d {V cY S w xY w|                     |||           d {V S )Nr   )cache_image_from_urlr  )gateway.platforms.baser  rI   r   
send_image_sidecar_send_attachment)	r   r   r  r  r  r  r  
local_pathr   s	           r"   r  zPhotonAdapter.send_image  s      	SCCCCCC33I>>>>>>>>JJ 	S 	S 	S++GYRRRRRRRRRRR	S 22Z 3 
 
 
 
 
 
 
 
 	
s    4AA
image_pathc                B   K   |                      |||           d {V S Nr  r  )r   r   r  r  r  r  kwargss          r"   send_image_filezPhotonAdapter.send_image_file  J       22Z 3 
 
 
 
 
 
 
 
 	
r,   
audio_pathc                D   K   |                      |||d           d {V S )Nr  )r  kindr  )r   r   r  r  r  r  r  s          r"   
send_voicezPhotonAdapter.send_voice  sL       22Zw 3 
 
 
 
 
 
 
 
 	
r,   
video_pathc                B   K   |                      |||           d {V S r  r  )r   r   r  r  r  r  r  s          r"   
send_videozPhotonAdapter.send_video  r  r,   	file_path	file_namec                D   K   |                      ||||           d {V S )N)r   r  r  )r   r   r  r  r  r  r  r  s           r"   send_documentzPhotonAdapter.send_document  sL       22YY 3 
 
 
 
 
 
 
 
 	
r,   animation_urlc                D   K   |                      |||||           d {V S r   )r  )r   r   r  r  r  r  s         r"   send_animationzPhotonAdapter.send_animation   sG       __]GXx
 
 
 
 
 
 
 
 	
r,   r  options	list[str]c                ^   K   |                      ||t          |pg                      d{V S )zSend a native iMessage poll through Photon Spectrum.

        Thin public wrapper over :meth:`_sidecar_send_poll`, the single
        implementation of the sidecar's ``/send-poll`` primitive (also used
        by the poll-backed clarify path).
        N)r  r  )r   r   r  r  r  s        r"   	send_pollzPhotonAdapter.send_poll-  s>       ,,WeT'-R=P=PQQQQQQQQQr,   effectc                  K   |                                 r|                                 st          dd          S 	 |                     d||                                 |                                 d           d{V }n5# t          $ r(}t          dt	          |                    cY d}~S d}~ww xY wt          d|                    d          	          S )
z9Send text with a native iMessage bubble or screen effect.Fztext and effect are requiredr  r   z/send-effect)spaceIdr   r  NTr  r  r  )r   r   r  rI   r0   r   )r   r   r   r  r  r   rP   s          r"   send_effectzPhotonAdapter.send_effect<  s       zz|| 	S6<<>> 	Se3QRRRR	;++#TZZ\\V\\^^TT       DD  	; 	; 	;e3q66:::::::::	;$488K3H3HIIIIs   AB 
B4B/)B4/B4c                <  K   t          j                     }|| j                            |d          z
  t          k     rd S || j        |<   	 |                     d|dd           d {V  d S # t
          $ r&}t                              d|           Y d }~d S d }~ww xY w)Nr@  /typingstartr  r  z[photon] send_typing failed: %s)r  rh  r   _TYPING_COOLDOWN_SECONDSr  rI   rJ   r   )r   r   r  r  rP   s        r"   send_typingzPhotonAdapter.send_typingO  s      ikk'++GS999<TTTF*-w'	?$$wAA            	? 	? 	?LL:A>>>>>>>>>	?s   
A+ +
B5BBc                   K   | j                             |d            	 |                     d|dd           d {V  d S # t          $ r&}t                              d|           Y d }~d S d }~ww xY w)Nr  stopr  z[photon] stop_typing failed: %s)rh  r  r  rI   rJ   r   )r   r   rP   s      r"   stop_typingzPhotonAdapter.stop_typing[  s      ""7D111	?$$w@@            	? 	? 	?LL:A>>>>>>>>>	?s   A   
A0
A++A0i  r   c                   |sd S | j         }||v r||= t          j                    ||<   t          |          | j        k    rDt	          |                                          d t          |          | j        z
           D ]}||= d S d S r   )re  r  r  _SENT_IDS_MAXr  r  )r   r  sentr  s       r"   _record_sent_messagez"PhotonAdapter._record_sent_messagem  s     	F%Z 9;;Zt99t)))DIIKK(()I3t99t7I+I)IJ  II *) r,   z^any;-;(\+\d{6,})$c                h    | j                             |          }|r|                    d          n|S )Nr   )_DM_CHAT_GUID_REr|  r  )clsr   r|  s      r"   _normalize_chat_keyz!PhotonAdapter._normalize_chat_key  s1    $**733!&3u{{1~~~G3r,   c                   |r|sd S |                      |          }| j        }||v r||= |||<   t          |          | j        k    rDt	          |                                          d t          |          | j        z
           D ]}||= d S d S r   )r  rf  r  _LAST_INBOUND_CHATS_MAXr  r  )r   r   r  keylastr  s         r"   r#  z"PhotonAdapter._record_last_inbound  s      	j 	F&&w//)$;;S	S	t99t333DIIKK((:#d))d:::   II	 43 r,   c                Z   |rt          |          sd S |                     |          }| j        }||v r||= t          j                    ||<   t	          |          | j        k    rDt          |                                          d t	          |          | j        z
           D ]}||= d S d S r   )r
  r  rg  r  r  r  r  r  )r   r   r   r  recentr  s         r"   r&  z%PhotonAdapter._record_recent_richlink  s     	1$77 	F&&w///&==sikksv;;555FKKMM**<#f++ <<<     3KK	 65   r,   c                
   |rt          |          sdS |                     |          }| j                            |          }|dS t	          j                    |z
  t
          k    r| j                            |d            dS dS )NFT)r.  r  rg  r   r  "_RICHLINK_PREVIEW_SUPPRESS_SECONDSr  )r   r   r  r  r  s        r"   r"  z)PhotonAdapter._is_recent_richlink_preview  s     	:7CC 	5&&w//-11#66<59;; BBB*..sD9995tr,   c                x    t          j        dd                                                                          dv S )NPHOTON_REACTIONSr   >   rC  rD  rE  r   r   r  s    r"   _reactions_enabledz PhotonAdapter._reactions_enabled  s:    y+W55;;==CCEE J
 
 	
r,   r
  c                   K   	 |                      d|||d           d{V  dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)zCTapback ``emoji`` onto a message. Soft-fails (False), never raises.z/react)r  r  r
  NTz [photon] add_reaction failed: %sFr  rI   rJ   r   )r   r   r  r
  rP   s        r"   _add_reactionzPhotonAdapter._add_reaction  s      	$$#*uMM         4 	 	 	LL;Q???55555	s    & 
AAAc                   K   	 |                      d||d           d{V  dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)a+  Retract our tapback from a message. Soft-fails (False), never raises.

        The sidecar tracks one reaction handle per target message; after a
        sidecar restart the handle is gone and removal is best-effort (the
        stale tapback self-heals when the next reaction replaces it).
        z/unreact)r  r  NTz#[photon] remove_reaction failed: %sFr  )r   r   r  rP   s       r"   _remove_reactionzPhotonAdapter._remove_reaction  s      	$$jII         4 	 	 	LL>BBB55555	s   % 
AAAc                   K   |p,| j                             |                     |                    }|sdddS |                     |||           d{V }|sdddS d|dS )uA  Tapback ``emoji`` onto a message in ``chat_id``.

        Without ``message_id``, targets the chat's most recent inbound
        message (typically the one the agent is responding to). iMessage
        maps ❤️👍👎😂‼️❓ to native tapbacks; anything else uses Apple's
        custom-emoji reaction.
        Fuk   no message to react to — pass message_id (no inbound message seen in this chat since the gateway started)r  Nz'reaction failed (see gateway debug log)Tr  )rf  r   r  r  )r   r   r
  r  targetr  s         r"   add_reactionzPhotonAdapter.add_reaction  s        
t9==$$W-- 
  
  	 O  
 %%gvu======== 	 B    v666r,   c                   K   |p,| j                             |                     |                    }|sdddS |                     ||           d{V }|sdddS d|dS )z1Retract our tapback from a message (best-effort).Fu)   no message to unreact — pass message_idr  Nz&unreact failed (see gateway debug log)Tr  )rf  r   r  r  )r   r   r  r  r  s        r"   remove_reactionzPhotonAdapter.remove_reaction  s        
t9==$$W-- 
  
  	 D   ((&99999999 	 A    v666r,   r   c                   K   |                                  sdS t          |j        dd          }t          |dd          }|r!|r!|                     ||d           d{V  dS dS dS )u=   Tapback 👀 on the triggering message while the agent works.Nr   r  u   👀)r  r   r  r  )r   r  r   r  s       r"   on_processing_startz!PhotonAdapter.on_processing_start		  s      &&(( 	F%,	488UL$77
 	Hz 	H$$Wj,GGGGGGGGGGG	H 	H 	H 	Hr,   u   👍u   👎c                   K   |d|dS )zReturn whatever we know about a Spectrum space id.

        Photon's ``space.id`` is opaque; the inbound event also carries the
        DM/group type, but here we only have the id, so infer conservatively.
        r  )r   r   r#  r^   )r   r   s     r"   get_chat_infozPhotonAdapter.get_chat_info	  s        W===r,   c                @    t                      r|S t          |          S r   )r   r   )r   r  s     r"   r  zPhotonAdapter.format_message"	  s%      	Ng&&&r,   r   c                    t          j        |           rdS | sdS |                                 dv sdv rdS t          fdt          D                       S )NTFzretryable=falseauth_or_configc              3      K   | ]}|v V  	d S r   r^   )rv  patlowereds     r"   rx  z4PhotonAdapter._is_retryable_error.<locals>.<genexpr>3	  s'      HHc3'>HHHHHHr,   )r   _is_retryable_errorr   ry  _PHOTON_RETRYABLE_PATTERNS)r   r  s    @r"   r  z!PhotonAdapter._is_retryable_error*	  st    2599 	4 	5++--''+;w+F+F5HHHH-GHHHHHHr,   r   c                    | j         }t          |t                    o-|                    d          du o|                    d          dv S )u  True when the sidecar classified the failure as permanent.

        ``auth_or_config`` and ``target_not_allowed`` cannot be fixed by
        retrying or by the plain-text fallback resend — attempting either
        just double-sends a doomed request (issue #50971).
        r}   Fr|   )r  r   )raw_responserW   rX   r   )r   rZ   s     r"   _is_permanent_sidecar_failurez+PhotonAdapter._is_permanent_sidecar_failure5	  sS     !sD!! S$$-S&&*RR	
r,   r   rG  max_retries
base_delayr   c                  K   |                      |          }|                     ||||           d{V }|j        r|S |                     |          r|S |j        pd}	|j        p|                     |	          }
|
s|                     |	          r|S |
rt          d|dz             D ]}|d|dz
  z  z  }t          
                    d||||	           t          j        |           d{V  |                     ||||           d{V }|j        r|c S |j        pd}	|                     |          r|c S |j        s|                     |	          s nt                              d||	           t          
                    d|	           |                     ||d| j                 d	d	
           d{V }|j        s t                              d|j                   |S )u   Retry sends without the generic Markdown banner.

        Photon replies are markdown (rendered by iMessage) or stripped plain
        text under ``PHOTON_MARKDOWN=false`` — either way the gateway's
        generic banner never applies.
        )r   r  r  r  Nr   r   r  z;[photon] Send failed (attempt %d/%d, retrying in %.1fs): %sz8[photon] Failed to deliver response after %d retries: %sz6[photon] Send failed: %s - retrying plain-text messageFr  markdownz)[photon] Plain-text retry also failed: %s)r  r  r  r  r   r}   r  r   rangerJ   rK   r   r  r  MAX_MESSAGE_LENGTH)r   r   r  r  r  r  r  r   r   	error_str
is_networkattemptdelayfallback_results                 r"   _send_with_retryzPhotonAdapter._send_with_retryD	  s      ""7++yy	 ! 
 
 
 
 
 
 
 
 > 	M--f55 	 ML&B	%L)A)A))L)L
 	d44Y?? 	M 	 K!O44  "aGaK&89Q[%   mE*********#yy# %%	  )           > "!MMM"L.B	55f== " "MMM( D,D,DY,O,O EN   	D	
 	
 	
 !% 2 2*4**+	 !3 !
 !
 
 
 
 
 
 
 & 	]LLDoF[\\\r,   r'  r  c                J  K   	 |                      d||d           d {V }n5# t          $ r(}t          dt          |                    cY d }~S d }~ww xY w|                     |                    d                     t          d|                    d                    S )N/send-richlinkr  r  Fr  r  Tr  )r  rI   r   r0   r  r   )r   r'  r  r   rP   s        r"   _sidecar_send_richlinkz$PhotonAdapter._sidecar_send_richlink	  s      	;++ hs"C"C       DD  	; 	; 	;e3q66:::::::::	;!!$((;"7"7888$488K3H3HIIIIs   $ 
AAAATr   r  r  c               b  K   |rt          |          nd }|rG|                     ||           d {V }|j        r|S t                              d|j                   d}t          |          | j        k    r=t                              dt          |          | j                   |d | j                 }||d}|rt                      rd|d<   	 | 	                    d|           d {V }ny# t          $ r<}	t          dt          |	          |	j        |	j        d|	j        	          cY d }	~	S d }	~	wt          $ r(}	t          dt          |	          
          cY d }	~	S d }	~	ww xY w|                     |                    d                     t          d|                    d                    S )Nz>[photon] rich-link send failed, falling back to plain text: %sFz0[photon] truncating outbound from %d to %d charsr  r   r  format/sendr{   r  r   r  r}   r  r  Tr  )r  r  r  rJ   rK   r   r  r  r   r  ry   r   r0   r|   r}   rI   r  r   )
r   r'  r   r  r  rich_urlrich_resultbodyr   rP   s
             r"   r  zPhotonAdapter._sidecar_send	  s'      19B&t,,,d 	 $ ; ;Hh O OOOOOOOK" #""NNP!   Ht99t...NNBD		42   1$112D+3TBB  	()++ 	('DN	;++GT::::::::DD! 		 		 		!ff#$=!"  +          	; 	; 	;e3q66:::::::::	;!!$((;"7"7888$488K3H3HIIIIs0   C, ,
E"61D-'E"-E":EE"E"r  c                @  K   d |pg D             }|r|                                 st          dd          S t          |          dk     rt          dd          S ||                                 d| j                 |d}	 |                     d	|           d{V }n5# t
          $ r(}t          dt          |                    cY d}~S d}~ww xY w|                     |                    d
                     t          d|                    d
                    S )zPOST a poll to the sidecar's ``/send-poll`` endpoint.

        Renders a native iMessage poll. ``options`` are choice strings; the
        sidecar's ``poll()`` builder degrades to a numbered text list on
        platforms without native polls.
        c                    g | ]D}t          |                                          #t          |                                          ES r^   )r0   r   )rv  os     r"   r<  z4PhotonAdapter._sidecar_send_poll.<locals>.<listcomp>	  s9    JJJ13q66<<>>JAJJJr,   Fzpoll title is requiredr  r  zpoll needs at least two optionsN)r  r  r  z
/send-pollr  Tr  )	r   r   r  r  r  rI   r0   r  r   )r   r'  r  r  optsr  r   rP   s           r"   r  z PhotonAdapter._sidecar_send_poll	  sO      KJBJJJ 	MEKKMM 	Me3KLLLLt99q==e3TUUUU[[]]#<T%<#<= 
  

	;++L$????????DD 	; 	; 	;e3q66:::::::::	;!!$((;"7"7888$488K3H3HIIIIs   B 
C)CCCr"  )r   	mime_typer  r  rL   r   r  r  c                 K   |                      t          |                    }|st          dd|           S |s ddl}|                    |          \  }	}
|	pd}|||dk    rdndd}|r||d	<   |r||d
<   |r||d<   	 |                     d|           d{V }ny# t          $ r<}t          dt          |          |j        |j        d|j                  cY d}~S d}~wt          $ r(}t          dt          |                    cY d}~S d}~ww xY w| 
                    |                    d                     t          d|                    d                    S )a  POST a local file to the sidecar's ``/send-attachment`` endpoint.

        ``kind`` is ``"voice"`` for audio sent as a voice note (downgrades
        to a plain audio attachment on platforms without voice notes),
        otherwise ``"attachment"``. spectrum-ts infers ``name`` and
        ``mimeType`` from the file extension; we only pass overrides when
        Hermes supplied them.
        Fz#unsafe or missing attachment path: r  r   Nr  r"  r  rL   r  r   r  r  /send-attachmentr{   r  r  Tr  )validate_media_delivery_pathr0   r   	mimetypes
guess_typer  ry   r|   r}   rI   r  r   )r   r'  rL   r   r  r  r  	safe_pathr  guessedr  r  r   rP   s                 r"   r  z&PhotonAdapter._sidecar_send_attachment	  s     * 55c$ii@@	 	%Q4%Q%Q     	("--i88JGQ4I#wGGL 
  

  	 DL 	)(D 	&%DO	;++,>EEEEEEEEDD! 		 		 		!ff#$=!"  +          	; 	; 	;e3q66:::::::::	;!!$((;"7"7888$488K3H3HIIIIs0   B 
D(1CDD,D	DDr  c                  K   | j         t          d          d| j         d| j         | }d| j        i}t          j        dd          4 d {V }|                    |||           d {V }d d d           d {V  n# 1 d {V swxY w Y   |j        d	k    rt          ||j        |j
                  |                                pi }|                    d
          st          ||j        |j
        |          |S )NzPhoton adapter not connectedr  r  r  rv   Fr  rD   r  r   r  )r]  r  rM  rK  rP  r   r  rU  r~   r   r   rD   r   )r   rL   r  r  r  r  r  r   s           r"   r  zPhotonAdapter._sidecar_call%
  s     $=>>> H*GGT-?GGG+T-@A$TUCCC 	F 	F 	F 	F 	F 	F 	FvStWEEEEEEEED	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	F 	Fs"".tT5EtyQQQyy{{ bxx~~ 	X.tT5EtyRVWWWs   B
BB)r1  r   )rZ   r   r$   rm  )r   r0   r$   r`   )r   r0   r$   r0   )r  r`   r$   r`   r$   r2   )r  r0   r$   r2   )r  r0   r$   r`   )r  r0   r  r0   r$   r2   )r  r  r$   r2   )r-   r.   r$   r5  )r1   r.   r$   r`   )rp  rq  r$   r2   )r$   r0   )r  r0   r$   r2   )NN)
r   r0   r  r0   r  r   r  rR   r$   r   r   )r   r0   r  r0   r  r  r  r0   r  r0   r  rR   r$   r   )NNN)r   r0   r  r0   r  r   r  r   r  rR   r$   r   )r   r0   r  r0   r  r   r  r   r  rR   r$   r   )r   r0   r  r0   r  r   r  r   r  rR   r$   r   )r   r0   r  r0   r  r   r  r   r  rR   r$   r   )NNNN)r   r0   r  r0   r  r   r  r   r  r   r  rR   r$   r   )r   r0   r  r0   r  r   r  r   r  rR   r$   r   )
r   r0   r  r0   r  r  r  rR   r$   r   )
r   r0   r   r0   r  r0   r  rR   r$   r   )r   r0   r$   r2   )r  r   r$   r2   )r   r0   r$   r0   )r   r   r  r   r$   r2   )r   r0   r   r0   r$   r2   )r   r0   r  r  r$   r`   r$   r`   )r   r0   r  r0   r
  r0   r$   r`   )r   r0   r  r0   r$   r`   )r   r0   r
  r0   r  r   r$   r  )r   r0   r  r   r$   r  )r  r   r$   r2   )r   r0   r$   r  )r  r0   r$   r0   )r   r   r$   r`   )r   r   r$   r`   )NNr   rG  )r   r0   r  r0   r  r   r  r   r  r.   r  r   r$   r   )r'  r0   r  r0   r$   r   )
r'  r0   r   r0   r  r`   r  r`   r$   r   )r'  r0   r  r0   r  r  r$   r   )r'  r0   rL   r0   r   r   r  r   r  r   r  r0   r$   r   )rL   r0   r  r  r$   r  )Lr   r   r   r   _MAX_MESSAGE_LENGTHr  SUPPORTS_MESSAGE_EDITINGr   staticmethodrj  rz  r  r  r  r  r  r  r  r  r  r  r  r?  rB  rD  r^  r  rk  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  compiler  classmethodr  r#  r&  r"  r  r  r  r  r  r  	_OK_EMOJI_FAIL_EMOJIr  r  r  r  r	  r  r  r  r  r  r   r   s   @r"   r0  r0    s         -  %C
 C
 C
 C
 C
 C
N 
 
 
 \
O O O O
   " 5: @ @ @ @ @ @D'" '" '" '"R!? !? !? !?FP P P P&1 &1 &1 &1P9 9 9 9vA A A A(   "   Z1 Z1 Z1 Z1| 	Q 	Q 	Q \	Q 
8 
8 
8 \
8    \B B B BHH
 H
 H
 H
T0 0 0 06G5 G5 G5 G5V! ! ! !!F !F !F !FF!^ !^ !^ !^F36 36 36 36j
' 
' 
' 
'$ #'-1O O O O O6 .2! ! ! ! ! ! !^ "&"&-1
 
 
 
 
 
 
. "&"&-1
 
 
 
 
" "&"&-1
 
 
 
 
" "&"&-1
 
 
 
 
" "&#'"&-1
 
 
 
 
$ "&"&-1
 
 
 
 
$ .2R R R R R( .2J J J J J&
? 
? 
? 
? 
?? ? ? ? M!	 	 	 	  "rz"7884 4 4 [4           
 
 
 

 
 
 

      4 %)	7 7 7 7 7> 9=7 7 7 7 7(H H H H IK> > > >' ' ' ' I I I \I 
 
 
 \
$ #'R R R R RhJ J J J ,J ,J ,J ,J ,J ,J\J J J J> ##'!% 9J 9J 9J 9J 9J 9Jv       r,   r0  r   r   c                N   | pd                                 } |                     d          rt          j        S |                     d          rt          j        S |                     d          rt          j        S |                     d          rt          j        S t          j        S )Nr   image/zvideo/audio/zapplication/)r   
startswithr   PHOTOVIDEOAUDIOr%  )r   s    r"   r  r  =
  s    JBDx   !  x   !  x   !  ~&& $##r,   .jpgz.pngz.gifz.webp)z
image/jpegz	image/pngz	image/gifz
image/webpz
image/heicz
image/heifz
image/tiff.mp3z.oggz.wavr  .m4a)z	audio/mp3z
audio/mpegz	audio/oggz	audio/wavr  r  z	audio/aacr  r   r  c                  |                      d          }|sdS 	 t          j        |          }n:# t          t          f$ r&}t
                              d|           Y d}~dS d}~ww xY wddlm}m	}m
}	 |pd                                }|rt          |          j        nd}
	 |                    d          rF|
pt                               |d          }	  |	||          S # t          $ r  |||          cY S w xY w|s|                    d	          r-|
pt                                ||rd
nd          } |||          S  |||          S # t"          $ r'}t
                              d||           Y d}~dS d}~ww xY w)a  Decode a base64-inlined inbound attachment and cache it locally.

    The sidecar inlines the attachment bytes as ``content["data"]`` (base64).
    We decode them and route to the shared media cache by MIME type, returning
    the cached absolute path so the caller can populate ``media_urls`` (which
    the gateway then hands to the model). Returns ``None`` when there are no
    bytes (over the sidecar's inline cap or a failed read) or when caching
    fails, so the caller can fall back to a text marker.
    r   Nz6[photon] failed to decode inbound attachment bytes: %sr   )cache_audio_from_bytescache_document_from_bytescache_image_from_bytesr   r/  r5  r0  r7  r6  z2[photon] failed to cache inbound attachment %s: %s)r   base64	b64decoderV   rd   rJ   rK   r  r9  r:  r;  r   r   r8   r1  _IMAGE_EXT_BY_MIME_AUDIO_EXT_BY_MIMErI   )r  r   r   r  data_b64rZ   r   r9  r:  r;  r8   exts               r"   r  r  `
  s     {{6""H tx((	"   OQTUUUttttt          JBD"&.T$ZZBF??8$$ 	<@.224@@C<--c3777 < < < 10d;;;;;<  	4$//(33 	4 .227ff C *)#s333((d333   KTSVWWWtttttsR   0 A'A""A'#2E C" "C;8E :C;;AE E 
E?E::E?r  c                   	 |                                  pi }n# t          $ r i }Y nw xY wt          |t                    si }t	          |                    d          pd          }t          |                    d                    }|dk    r
t          }d}nJ| j        dk    rd| j         d| j	        d	d          }n$t	          |                    d
          pd          }|||dS )a9  Build the structured error dict for a failed standalone sidecar call.

    Mirrors ``_sidecar_error_from_response``: parse the sidecar body
    independently, carry the error class + retryability, and swap in the
    canonical user-facing message for ``target_not_allowed`` (never the raw
    upstream text).
    r|   rz   r}   r   Fr   zsidecar returned z: Nr   zsidecar reported failure)r   r|   r}   )
rD   rI   rW   rX   r0   r   r`   r   r~   r   )r  r   r|   r}   r   s        r"   _standalone_errorrC  
  s   yy{{ b   dD!! dhh}--@AAKTXXk**++I***+				S	 	 ID$4II	$3$IIDHHW%%C)CDD;YOOOs    (()	thread_idmedia_filesforce_documentpconfigr   r  rD  rE  r  rF  c               >  K   t           sddiS t          | j        pi                     d          pt	          j        d          t                    }t          d          }|st                      }d}	|r|                    d          rtt          |                    d                    r9t          |d                   }t          |                    d	          |          }nd
|                    d           d}	|sddt           d|	z   iS dt           d| }
d|i}d }	 t          j        dd          4 d {V }|rjt          |          }|ru|                    |
 d||d|           d {V }|j        dk    rD|                                pi }|                    d          r|                    d          }nd }nd }|s||d t&                   d}t)                      rt          |          sd|d<   |                    |
 d||           d {V }|j        dk    r!t+          |          cd d d           d {V  S |                                pi }|                    d          s!t+          |          cd d d           d {V  S |                    d          }dd l}|pg D ]!\  }}t/          j        t          |                    }|st2                              d           D|                    |          \  }}|||rd nd!d"}|r||d#<   |                    |
 d$||           d {V }|j        dk    r#t+          |          c cd d d           d {V  S |                                pi }|                    d          s#t+          |          c cd d d           d {V  S |                    d          p|}#	 d d d           d {V  n# 1 d {V swxY w Y   d%|d&S # t8          $ r}dd'| icY d }~S d }~ww xY w)(Nr   r  r4  r5  r6  r   r/   r1   r-   z/ A stale sidecar runtime record was found (pid u4    is not running) — the gateway appears to be down.zPhoton standalone send requires a running sidecar. Start the Hermes gateway (which spawns the sidecar and records its address under <hermes-home>/runtime/z=), or set PHOTON_SIDECAR_TOKEN in this process's environment.r  r  r  rv   Fr  r  r  r$  r   r  r  r  r  r  r  r   z-[photon] standalone send skipping unsafe pathr  r"  r  r  r  Tr  zPhoton standalone send failed: )r   r   r   r   r   r   rJ  r#   r[   rj   r0   r*   rL  r   r  r  rU  r~   rD   r'  r   rC  r  r   r  rJ   rK   r   rI   )rG  r   r  rD  rE  rF  r-   r/   record
stale_hintbaser  last_message_idr  r  r  r   	send_bodyr  
media_pathr  r!  r"  r  att_bodyrP   s                             r"   _standalone_sendrP  
  s        0.//		"!!.11URY?T5U5U D 566E 
 &''
 		fjj)) 		!&**U"3"344 F7O,,#FJJv$6$6==*zz%((* * * 
  		5 ,5 5 5 8B	B  4*33T33D'/G%)OB@$TUCCC =	K =	K =	K =	K =	K =	K =	Kv <.w77 (!'///)0BB ' "- " "      D
 '3..#yy{{0b88D>> ,.2hh{.C.COO'+HH#' <#* '(<)<(< =1 1I )** 93Fw3O3O 9.8	(+!'Y "- " "      D '3..066;=	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K<  99;;,"D88D>> 7066A=	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	KB '+hh{&;&;O
 (3(9r K K$
H/LSQ[__]]	  NN#RSSS&11)<<
&%'/AGG\, ,
  3+2HZ(#[[---Hg )         #s**,T2222s=	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	Kt yy{{(bxx~~ 3,T2222y=	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	Kz #'((;"7"7"J?+KQ=	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K =	K~  ??? @ @ @>1>>???????@st   #P ?C:O+9P 9O+P CO+P 1;O+,P ?O+P +
O55P 8O59P 
PPPPc                    ddl m} |                     ddd t          t          t
          ddgd|j        t          d	t          d
dt          dddd           | 
                    dd|j        |j                   dS )z.Called by the Hermes plugin loader at startup.r   )clir3  ziMessage via Photonc                     t          |           S r   )r0  r   s    r"   <lambda>zregister.<locals>.<lambda>5  s    M#$6$6 r,   r   r   zRun: hermes photon setup  (logs in via device flow, creates a Spectrum project, links your phone number, installs the spectrum-ts sidecar).r   PHOTON_ALLOWED_USERSPHOTON_ALLOW_ALL_USERSu   📱Tua  You are communicating via Photon Spectrum (iMessage). Treat replies like regular text messages — short and friendly. Markdown is rendered (bold, italics, lists, code), but keep formatting light and conversational. Recipient identifiers are E.164 phone numbers; never expose them in responses unless the user asked. Attachments arrive as metadata only.)r   r  adapter_factorycheck_fnr   r   required_envinstall_hintsetup_fnenv_enablement_fncron_deliver_env_varstandalone_sender_fnallowed_users_envallow_all_envmax_message_lengthr
  pii_safeallow_update_commandplatform_hintz1Set up and manage the Photon iMessage integration)r   helpr[  
handler_fnN)r   rR  register_platformr   r   r   gateway_setupr   rP  r'  register_cli_commandregister_clidispatch)ctx_clis     r"   registerrn  ,  s     #66#'!)+BC$ #)2-0.. !?;  $ $ $N @"=	      r,   r   )r$   r   )r-   r.   r/   r0   r1   r.   r$   r2   )r$   rR   r%  )r1   r   r$   r`   )
rL   r0   r~   r.   r   r0   r   rR   r$   ry   )r   r   r    r.   r$   r.   r&  )r   r   r    r   r$   r   )r   r   r$   r   )r   rG   r$   r`   )r   r   r$   r`   )r$   r   )r   r0   r$   r   )r  r  r$   r0   )r  r  r$   r   )r   r  r$   r`   )r  r  r$   r`   )r   r0   r$   r   )
r  r  r   r0   r   r0   r  r`   r$   r   )r  r   r$   r  )rG  r   r   r0   r  r0   rD  r   rE  r  rF  r`   r$   r  )qr   
__future__r   r   r<  rD   loggingr   r  rN  r   rX  r   rS  r  r   r   pathlibr   typingr   r   r	   r
   r   urllib.parser   r   r   ImportErrorgateway.configr   r   r  r   r   r   r   r   gateway.platforms.helpersr   r   authr   agent.secret_scoper   r   r   r   r#   	getLoggerr   rJ   rJ  rL  r'  r*   r+   rQ   r[   r_   rj   r  r  r  sidecar_pathsrl   r   rm   rn   __annotations__ro   r   rq   rs   r   r  r  r%  r  rr  r  ry   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r
  r  r  r  r(  r,  r.  r0  r  r>  r?  r  rC  rP  rn  r^   r,   r"   <module>r|     s    0 # " " " " "     				 				        



  ' ' ' ' ' ' ' '       ; ; ; ; ; ; ; ; ; ; ; ; ; ; ! ! ! ! ! !  LLLOO    4 3 3 3 3 3 3 3              O N N N N N N N * * * * * * J J J J J J ? ? ? ? ? ?/ / / /( 
	8	$	$
  # 
   - @ @ @ @Q Q Q Q>2 2 2 2      @ !   N M M M M M M M  $ # # # #!% % % % %    4 4 4 4  
  &* "&@ #
   +" 
 
 
 
 
 
 
 
6 &*	    BN 
   
F 
F 
F 
F      
 
 
 
3 3 3 33 3 3 3l   $8O 8O 8O 8Ov             &
 
 
 

 
 
 
	% 	% 	% 	%T T T T   $   "      ,B B B B B' B B BP<
  
  
  
          4 4 4 4 4 4xP P P P>  $"& p@ p@ p@ p@ p@ p@l2 2 2 2 2 2s   &A- -	A98A9