
    `gj                    :   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mZ ddl	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Z  ej.                  e      Z	 ddlZeduZd
Zd
ZdZddZ ddZ!ddZ"e G d d             Z#ddZ$ G d d      Z%y# e$ r dZY =w xY w)u  Production WebSocket RelayTransport — the gateway's live link to the connector.

The gateway dials OUT to the connector's relay endpoint over a WebSocket and
speaks the newline-delimited JSON frame protocol defined in the connector repo
(``gateway-gateway`` ``src/relay/protocol.ts``) and mirrored in
``docs/relay-connector-contract.md``:

  gateway -> connector : hello, outbound, interrupt
  connector -> gateway : descriptor, inbound, outbound_result, interrupt_inbound

Frames:
  hello            {type, platform, botId}
  descriptor       {type, descriptor}                       (handshake reply)
  inbound          {type, event, bufferId?}                 (a normalized MessageEvent)
  outbound         {type, requestId, action}                (send/edit/typing/follow_up)
  outbound_result  {type, requestId, result}
  interrupt        {type, session_key, reason?}             (gateway egresses /stop)
  interrupt_inbound{type, session_key, chat_id}             (connector -> owning gateway)

This is the concrete transport behind the ``RelayTransport`` Protocol; the
``RelayAdapter`` delegates all wire I/O to it. Outbound calls block on a
per-request future keyed by ``requestId`` until the matching ``outbound_result``
arrives. A background reader task pumps inbound frames to the registered handler
and resolves pending outbound futures.

EXPERIMENTAL: the frame schema may change without a deprecation cycle until at
least two Class-1 platforms validate it.
    )annotationsN)	dataclass)AnyDictListOptional)MessageEventMessageType)SessionSource)CapabilityDescriptor)InboundHandler      >@i1  c                   | xs dj                         }|j                  d      rd|t        d      d z   }n"|j                  d      rd|t        d      d z   }|j                  d      }|j	                  d      s| d}|S )	u  Normalize a connector URL to the ``ws(s)://…/relay`` dial target.

    The relay URL is configured once (``GATEWAY_RELAY_URL`` / ``gateway.relay_url``)
    as the connector's BASE URL (e.g. ``https://connector.example``) and shared by
    both the provision POST (which needs ``http(s)://…/relay/provision`` — see
    ``_provision_url``) and the WS dial (which needs ``ws(s)://…/relay``, the path
    the connector mounts its ``WebSocketServer`` on). Two normalizations, both
    load-bearing:

      - scheme: ``https -> wss``, ``http -> ws`` (``websockets.connect`` raises
        "scheme isn't ws or wss" on an http(s) URL).
      - path: ensure it ends in ``/relay`` (the connector returns HTTP 400 on an
        upgrade to any other path, since the WS server is mounted at ``/relay``).

    Idempotent: an already-``ws(s)://…/relay`` URL is returned unchanged, so a URL
    configured WITH the scheme and/or ``/relay`` still works.
     zhttps://zwss://Nzhttp://zws:///z/relay)strip
startswithlenrstripendswith)urlraws     M/root/.hermes/venv/lib/python3.12/site-packages/gateway/relay/ws_transport.py_ws_dial_urlr   A   s    $ 9"


C
~~j!S_-..			"C	NO,,
**S/C<<!VnJ    c                   | rt        | t              syg }| D ]  }t        |t              s|j                  d      }|s(|j                  d      xs i }d}t        |t              r(|j                  d      xs |j                  d      xs d}|j	                  |r| d| n
t        |              |sydj                  |      }d	| S )
u  Render the connector's read-only surrounding-context array into the string
    ``MessageEvent.channel_context`` field.

    The connector attaches ``context`` as a list of normalized message objects
    (oldest→newest, same channel) for an addressed turn on a context-capable
    platform (design relay-channel-context). We flatten each to a
    ``<author>: <text>`` line so it rides the SAME read-only injection path that
    history-backfill already uses (run.py prepends ``channel_context`` ahead of
    the trigger message). This is REFERENCE context only — it never triggers the
    agent; the trigger decision was already made connector-side on the addressed
    event alone.

    Returns None when there is no usable context (absent/empty list, or a
    connector that doesn't send the field), so ``channel_context`` stays unset
    and behaviour is byte-identical to today. Never raises — a malformed context
    payload must not break inbound delivery of the (already-admitted) turn.
    Ntextsourcer   	user_nameuser_idz: 
z[Recent channel messages]
)
isinstancelistdictgetappendstrjoin)contextlinesitemr   srcauthorbodys          r   _render_relay_contextr/   ^   s    $ *Wd3E 
C$%xxhhx &Bc4 WW[)ESWWY-?E2FFxr$(D	B
C 99UD(//r   c                   | j                  di       xs i }ddlm} |j                  dd      }	  ||      }t        ||j                  dd      |j                  dd	      |j                  d
      |j                  d      |j                  d      |j                  d      |j                  d      |j                  d      |j                  d      |j                  d      |j                  d      |j                  d      |j                  d      d      }	 t        | j                  dd            }t        | j                  dd      ||| j                  d      | j                  d      | j                  d      xs g t        | j                  d                  S # t        $ r |j                  }Y ~w xY w# t        $ r t        j                  }Y w xY w)u   Rebuild a MessageEvent from the connector's normalized inbound payload.

    The connector emits SessionSource as the snake_case wire form (§3); map it
    back onto the gateway dataclasses. Unknown message types fall back to TEXT.
    r   r   )Platformplatformrelaychat_idr   	chat_typedm	chat_namer    r   	thread_id
chat_topicuser_id_altchat_id_altscope_idparent_chat_id
message_idprofileT)r2   r4   r5   r7   r    r   r8   r9   r:   r;   r<   r=   r>   r?   delivered_via_upstream_relaymessage_typer   reply_to_message_id
media_urlsr)   )r   rA   r   r>   rB   rC   channel_context)
r%   gateway.configr1   
ValueErrorRELAYr   r
   TEXTr	   r/   )r   r,   r1   r2   platform_enumr   msg_types          r   _event_from_wirerK      s    ''(B

%2C'wwz7+H' * 	2&''+t,''+&	"''+&''+&77<(GGM*GGM*$ww/077<( 	" &*=F@$sww~v>? WWVR 77<(GG$9:77<(.B .cggi.@A Q  ' 'J  $##$s#   F F: F76F7:GGc                  N    e Zd ZU dZded<   ded<   ded<   ded<   ded<   d	ed
<   y)PassthroughForwardu  A connector-forwarded passthrough-plane request (Phase 5 §5.1).

    The connector answered the provider's latency-critical ACK at its edge, then
    forwarded the real (already-sanitized) request to this gateway over the WS.
    ``body`` is the exact decoded bytes the connector forwarded (the wire carries
    it base64-encoded for byte parity). ``headers`` preserve arrival order.
    r'   r2   bot_idmethodpathzlist[tuple[str, str]]headersbytesr.   N)__name__
__module____qualname____doc____annotations__ r   r   rM   rM      s(     MKK
I""
Kr   rM   c                F   ddl }| j                  dd      xs d}	 |j                  |      }| j                  dg       xs g }g }|D ]S  }t	        |t
        t        f      st        |      dk(  s)|j                  t        |d         t        |d         f       U t        t        | j                  d	d            t        | j                  d
d            t        | j                  dd            t        | j                  dd            ||      S # t        $ r d}Y w xY w)a^  Rebuild a PassthroughForward from the connector's wire frame.

    Mirrors the connector's ``PassthroughForward`` (relay/protocol.ts): the body
    is base64-decoded back to the exact bytes the connector forwarded, so the
    gateway re-processes byte-identical content (the connector is the trust
    boundary; it already verified at the edge).
    r   NbodyB64r   r   rQ         r2   botIdrO   rP   )r2   rN   rO   rP   rQ   r.   )base64r%   	b64decode	Exceptionr"   r#   tupler   r&   r'   rM   )r   r^   body_b64r.   headers_rawrQ   pairs          r   _passthrough_from_wirere      s    wwy"%+H) '')R(.BK%'G 9dT5M*s4yA~NNCQL#d1g,789 SWWZ,-3777B'(3778R()$%   s   D D D c            	      d   e Zd ZdZdeedddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d dZd!dZd"d	Zd#d
Z	d"dZ
d$dZed!d       Zd%dZdd	 	 	 	 	 d&dZdd	 	 	 	 	 d&dZd'dZd(dZd)d*dZd+d,dZd+d,dZd-dZ	 d.dd	 	 	 	 	 	 	 d/dZd0dZd"dZed1d       Zd"dZd2dZd3dZd3dZy)4WebSocketRelayTransportzNRelayTransport over a WebSocket connection the gateway dials to the connector.NF      ?r   )
identitiesconnect_timeout_soutbound_timeout_s
gateway_idupgrade_secret	reconnectreconnect_backoff_sreconnect_max_backoff_sc                  t         st        d      t        |      | _        || _        || _        |rt        |      n||fg| _        || _        || _	        || _
        || _        |	| _        |
| _        || _        d | _        d| _        d| _        d | _        d | _        d | _        d | _        d | _        i | _        d | _        d| _        d| _        d| _        y )NzXWebSocketRelayTransport requires the 'websockets' package (install the messaging extra).Frh   )WEBSOCKETS_AVAILABLERuntimeErrorr   _url	_platform_bot_idr#   _identities_connect_timeout_s_outbound_timeout_s_gateway_id_upgrade_secret
_reconnect_reconnect_backoff_s_reconnect_max_backoff_s_supervisor_dormant_dormant_redial_s_ws_reader_inbound_descriptor_descriptor_ready_pending_going_idle_ack_closing_handshake_succeeded_auth_revoked)selfr   r2   rN   ri   rj   rk   rl   rm   rn   ro   rp   s               r   __init__z WebSocketRelayTransport.__init__   s     $1  !%	! 0:4
+6@R?S"3#5  &- $$7!(?%9=  "%5926;?NRCE<@ %*!"r   c                @   K   | j                          d {    y7 w)NT)_dial_and_startr   s    r   connectzWebSocketRelayTransport.connect\  s      ""$$$ 	%s   c                  K   t        j                         }|j                         | _        d| _        d| _        | j                         }|r/t        j                  | j                  |       d{   | _
        n,t        j                  | j                         d{   | _
        t        j                  | j                         d      | _        | j                  D ]"  \  }}| j                  d||d       d{    $ y7 7 g7 w)zvOpen the socket, start the reader, send hello. Used by connect() and
        by the reconnect supervisor on a re-dial.NF)additional_headerszrelay-ws-readernamehello)typer2   r]   )asyncioget_running_loopcreate_futurer   r   r   _upgrade_headers
websocketsr   rt   r   create_task
_read_loopr   rw   _send)r   looprQ   r2   rN   s        r   r   z'WebSocketRelayTransport._dial_and_start`  s      '')!%!3!3!5   '')'//		gVVDH'//		::DH**4??+<CTU !% 0 0 	WHf**g8fUVVV	W W: Ws7   A.D	0D1,D	DAD	;D<D	D	D	c                    | j                   r| j                  si S ddlm}  || j                  | j                         }dd| iS )a  Auth headers for the WS upgrade, or {} when no secret is configured.

        Presents ``Authorization: Bearer *** where the token is a signed
        bearer built with the per-gateway secret (``gateway/relay/auth.py``
        ``make_upgrade_token``), keyed by ``gateway_id`` so the connector can
        index its verify list. The connector rejects the upgrade (close 4401)
        when this is missing/invalid/revoked; an unauthenticated connector
        ignores it.
        r   )make_upgrade_tokenAuthorizationzBearer )r{   rz   gateway.relay.authr   )r   r   tokens      r   r   z(WebSocketRelayTransport._upgrade_headersz  sG     $$)9)9I9"4#3#3T5I5IJ75'!233r   c                  K   d| _         | j                  6| j                  j                          	 | j                   d {    d | _        | j                  6| j                  j                          	 | j                   d {    d | _        | j                  *	 | j                  j                          d {    d | _        | j                  j                         D ]-  }|j                         r|j                  t        d             / | j                  j                          | j                  @| j                  j                         s%| j                  j                  t        d             y y y 7 5# t        j                  t
        f$ r Y Ow xY w7 # t        j                  t
        f$ r Y 0w xY w7 # t
        $ r Y w xY ww)NTzrelay transport closed)r   r   cancelr   CancelledErrorr`   r   r   closer   valuesdoneset_exceptionrs   clearr   )r   futs     r   
disconnectz"WebSocketRelayTransport.disconnect  s    '##%&&&&  $D<<#LL!ll""  DL88hhnn&&& DH=='') 	JC88:!!,/G"HI	J 	+D4H4H4M4M4O  ..|<T/UV 5P+- '**I6  #**I6 
 ' s   .GE8  E5E8 -G3F FF GF> 8F;9F> =4G2BG5E8 8FGFGF F84G7F88G;F> >	GG
GGc                   K   | j                   | j                   S | j                  t        d      t        j                  | j                  | j
                         d {   S 7 w)Nz#handshake() called before connect()timeout)r   r   rs   r   wait_forrx   r   s    r   	handshakez!WebSocketRelayTransport.handshake  s[     '###!!)DEE%%d&<&<dF]F]^^^^s   AA' A%!A'c                    | j                   S )u.  True once the connector closed the socket with 4401 AFTER a prior
        successful handshake — i.e. the per-gateway secret was revoked (the
        operator opted this instance out of the relay). Terminal: the transport
        stops reconnecting, and the adapter surfaces a clean "disabled" state.)r   r   s    r   auth_revokedz$WebSocketRelayTransport.auth_revoked  s     !!!r   c                    || _         y N)r   r   handlers     r   set_inbound_handlerz+WebSocketRelayTransport.set_inbound_handler  s	    r   r2   c               D   K   | j                  ||       d {   S 7 wNr   _request_responser   actionr2   s      r   send_outboundz%WebSocketRelayTransport.send_outbound  s$      ++FX+FFFF     c               D   K   | j                  ||       d {   S 7 wr   r   r   s      r   send_follow_upz&WebSocketRelayTransport.send_follow_up  s$      ++FX+FFFFr   c                D    |sy| j                   D ]  \  }}||k(  s|c S  y)un  The bot_id this transport advertised at hello for ``platform`` (Phase 1.5).

        The connector validates a per-frame egress target against the SET of
        ``platform:botId`` pairs it accumulated from the N hellos, so a per-frame
        ``platform`` must ride with its MATCHING ``botId`` (the session default
        botId belongs to the first identity and would mis-key for a second
        platform). Resolved from the identity set this transport was built with.
        None when the platform isn't one we front (the connector then rejects it
        with a structured failure — never a wrong-credential send).N)rw   )r   r2   pbs       r   _bot_id_forz#WebSocketRelayTransport._bot_id_for  s4     $$ 	DAqH}	 r   c                   K   | j                  d|dd       d {   }|j                  d      xs |}|j                  d|      |j                  dd      d	S 7 >w)
Nget_chat_info)opr4   outbound)
frame_type	chat_infor   r   r6   )r   r   )r   r%   )r   r4   resultinfos       r   r   z%WebSocketRelayTransport.get_chat_info  se     --"w7J . 
 
 zz+&0&1488FD;QRR
s   AA?Ac                J   K   | j                  d||d       d {    y 7 w)N	interrupt)r   session_keyreason)r   )r   r   r   s      r   send_interruptz&WebSocketRelayTransport.send_interrupt  s      jj+kU[\]]]s   #!#c                ~  K   | j                   yt        j                         }|j                         | _        	 | j                  ddi       d{    t        j                  | j                  |       d{    	 d| _        y7 67 # t        j                  t        f$ r
 Y d| _        yw xY w# d| _        w xY ww)u  Ask the connector to flip this instance's destination to buffered-only.

        Sends ``going_idle`` and awaits the connector's ``going_idle_ack`` — the
        connector-AUTHORITATIVE confirmation that live delivery has stopped and
        subsequent inbound buffers durably (Q-5.3c). Returns True on ack, False on
        timeout / not-connected (the caller proceeds to close anyway — at worst a
        live event races a closing socket exactly as before §5.3, no regression).

        The gateway stays serving (the read loop keeps handling inbound) until the
        ack, so an event landing in the flip window is delivered live, not lost.
        NFr   
going_idler   T)	r   r   r   r   r   r   r   TimeoutErrorr`   )r   	timeout_sr   s      r   go_idlezWebSocketRelayTransport.go_idle  s      88'')#113	(**fl3444""4#7#7KKK $(D  5K$$i0 	#'D 	 $(D s^   7B=B B(B 9B	:B ?B=B 	B B.$B1 %B=-B..B1 1	B::B=c                  K   | j                   y| j                  |       d{   }d| _        	 | j                   j                          d{    |S 7 07 # t        $ r t
        j                  dd       Y |S w xY ww)u=  Quiesce this transport for a scale-to-zero suspend (D12 / Phase 0).

        Distinct from BOTH ``disconnect()`` and an unexpected close (F14):
          - ``disconnect()`` sets ``_closing=True`` and CANCELS the reconnect
            supervisor — terminal, "shutting down for good." A machine suspended
            after that never re-dials on wake, so its buffered backlog strands.
          - An unexpected close re-dials IMMEDIATELY (fast backoff) — the socket
            never stays down, so the platform proxy never sees the connection go
            away and never suspends the machine.

        ``go_dormant()`` is the third mode the suspend behaviour needs:
          1. ``go_idle()`` → the connector flips this instance to buffered-only
             and acks (so inbound that arrives while we sleep buffers durably and
             replays on the next handshake).
          2. Close the socket so the platform proxy sees load drop to zero (the
             precondition for Fly ``autostop:"suspend"``) — but WITHOUT setting
             ``_closing``. The reader's normal end-of-socket fall-through still
             arms the reconnect supervisor, so the wake path stays live; the
             ``_dormant`` flag just makes that supervisor poll on the dormant
             cadence rather than fight the suspend window.

        On resume (process unfrozen) the supervisor's pending wait completes, the
        re-dial succeeds, and the connector drains the buffered backlog on the new
        handshake. Returns the ``go_idle`` ack result (True on ack); the dormancy
        close happens regardless (a missed ack at worst races one live event onto
        a closing socket, exactly as §5.3 already tolerates).

        No-op-safe: a transport that never connected (``_ws is None``) just
        returns False without closing.
        NF)r   Tz#relay go_dormant: ws.close() raised)exc_info)r   r   r   r   r`   loggerdebug)r   r   ackeds      r   
go_dormantz"WebSocketRelayTransport.go_dormant  s     > 88llYl77 	O((.."""  8 # 	OLL>LN	OsD   #BABA AA BA  A>:B=A>>Bc                   K   	 | j                  d|d       d{    y7 # t        $ r t        j                  d|       Y yw xY ww)u/  Acknowledge durable receipt of a buffered inbound delivery (§5.3).

        Sent after the adapter has durably taken a buffered inbound event the
        connector replayed on reconnect; the connector acks the buffer entry only
        after this, giving drain-without-dup on the delivery leg.
        inbound_ack)r   bufferIdNz%relay: inbound_ack send failed for %s)r   r`   r   r   )r   	buffer_ids     r   _send_inbound_ackz)WebSocketRelayTransport._send_inbound_ack+  sB     	M**mKLLL 	MLL@)L	Ms1   A# !# A# AAAAc                 K   | j                   dddS t        j                         j                  }t	        j
                         }|j                         }|| j                  |<   |||d}|r||d<   | j                  |      }|r||d<   	 | j                  |       d {    t	        j                  || j                         d {   | j                  j                  |d        S 7 I7 "# t        j                  $ r$ dddcY | j                  j                  |d        S w xY w# | j                  j                  |d        w xY ww)	NFrelay transport not connected)successerror)r   	requestIdr   r2   r]   r   zrelay outbound timed out)r   uuiduuid4hexr   r   r   r   r   r   r   ry   popr   )	r   r   r   r2   
request_idr   r   framerN   s	            r   r   z)WebSocketRelayTransport._request_response7  s-     88$/NOOZZ\%%
'').2.@.@.B$'j!)3*X^ _  (E*%%h/F!'g	0**U### ))#t7O7OPP MMj$/ $P## 	K$/IJJMMj$/	K MMj$/s`   BEC- C) (C- C+	C- E)C- +C- -D$D' E#D$$D' 'EEc                   K   | j                   t        d      | j                   j                  t        j                  |      dz          d {    y 7 w)Nr   r!   )r   rs   sendjsondumps)r   r   s     r   r   zWebSocketRelayTransport._sendY  s@     88>??hhmmDJJu-4555s   AAAAc                \  K   | j                   J d}	 | j                   2 3 d {   }|t        |t              r|n|j                  d      z  }|j	                  d      ^ }}|D ],  }|j                         s| j                  |       d {    . u7 p7 	6 n# t        j                  $ r  t        $ ry}| j                  |      t        k(  r5| j                  r)d| _        | j                  s8t        j!                  d       n"| j                  st        j!                  d|       Y d }~nd }~ww xY w| j"                  rl| j                  s_| j                  sR| j$                  | j$                  j'                         r+t        j(                  | j+                         d      | _        y y y y y w)	Nr   zutf-8r!   Tu   relay ws closed 4401 (unauthorized) after a successful handshake — treating as a revoked relay credential (opt-out); not reconnectingzrelay ws read loop ended: %szrelay-ws-reconnectr   )r   r"   r'   decodesplitr   _handle_framer   r   r`   _close_code_of_RELAY_UNAUTHORIZED_CLOSE_CODEr   r   r   r   warningr|   r   r   r   _reconnect_loop)r   bufchunkr*   lineexcs         r   r   z"WebSocketRelayTransport._read_loop^  s    xx###	D#xx 7 7e
5# 6uELL<QQ!iio! 7Dzz|"006667	7 7  ( %% 	 	D ""3'+IIdNgNg%)"}}NN] ]]=sC	D0 OOMM&&!!)T-=-=-B-B-D&22$$&-A D .E ' " si   F,B BBBAB 9B BB BB B F,D07A/D+&F,+D00A<F,c                    dD ]0  }t        | |d      }t        |dd      }t        |t              s.|c S  t        | dd      }t        |t              r|S dS )a  Best-effort extraction of a WebSocket close code from a raised
        exception. websockets' ConnectionClosed* expose the peer's Close frame
        via `.rcvd`/`.sent` (preferred; `.code` is deprecated in websockets 13+).
        Returns None when unknown.)rcvdsentNcode)getattrr"   int)r  attrr   fcoder  s        r   r   z&WebSocketRelayTransport._close_code_of  sb     % 	DCt,EE640E%%		
 sFD)!$,t6$6r   c                &  K   | j                   r| j                  n| j                  }| j                  s[	 t	        j
                  |       d{    | j                  ry	 | j                          d{    t        j                  d       yyy7 B# t        j                  $ r  w xY w7 6# t        j                  $ r  t        $ r9}t        j                  d|       t        |dz  | j                        }Y d}~nd}~ww xY w| j                  s݌w)u  Re-dial the connector with capped exponential backoff until reconnected
        or disconnect() is called. NET-NEW for §5.3: a re-established socket makes
        the connector replay this instance's buffered-only backlog on the new
        handshake (the delivery-leg onResume). Never raises out (a re-dial failure
        just retries); ends when a dial succeeds (its reader takes over) or closing.

        scale-to-zero (D12): when the close was a deliberate go_dormant() rather
        than an unexpected drop, start from the dormant poll cadence. On a
        suspended machine the event loop is frozen, so this sleep only advances
        once the machine is awake — it just needs to be short enough that a
        freshly-woken machine re-dials promptly. A successful _dial_and_start()
        clears _dormant, so any LATER unexpected drop reconnects on the normal
        fast backoff.Nzrelay ws reconnectedzrelay ws reconnect failed: %sr[   )r   r   r}   r   r   sleepr   r   r   r   r`   r   minr~   )r   backoffr  s      r   r   z'WebSocketRelayTransport._reconnect_loop  s      -1MM$((t?X?X--mmG,,, }}J**,,,23  ,)) 
 - ))  J>Dgk4+H+HIJ --sj   1DB BB DB+ 2B)3B+ DB B&&D)B+ +D /C;6D;D  Dc                p  K   	 t        j                  |      }|j                  d      }|dk(  rt        j                  t        j                  |j                  di                   }|| _	        d| _
        | j                  7| j                  j                         s| j                  j                  |       y y y |dk(  rx| j                  kt        |j                  di             }| j                  |       d {    |j                  d      }|r#| j!                  t#        |             d {    y y y |dk(  rD| j$                  7| j$                  j                         s| j$                  j                  d        y y y |d	k(  ra| j&                  j                  |j                  d
d            }|3|j                         s"|j                  |j                  di              y y y |dk(  rBt)        | dd       }|2 ||j                  dd      |j                  dd             d {    y y |dk(  rLt)        | dd       }|<t+        |j                  di             }	 ||	|j                  d             d {    y y y # t         j                  $ r t        j	                  d       Y y w xY w7 7 7 7 =w)Nzrelay: skipping malformed framer   
descriptorTinboundeventr   going_idle_ackoutbound_resultr   r   r   interrupt_inbound_interrupt_inbound_handlerr   r4   passthrough_forward_passthrough_handlerforward)r   loadsJSONDecodeErrorr   r   r%   r   	from_jsonr   r   r   r   r   
set_resultr   rK   r   r'   r   r   r  re   )
r   r  r   ftyper  r  r   r   r   fwds
             r   r   z%WebSocketRelayTransport._handle_frame  s    	JJt$E 		&!L -77

599\[]C^8_`J)D
 )-D%%%1$:P:P:U:U:W&&11*= ;X1i}}((7B)?@mmE***
 "IIj1	00Y@@@  ) && ##/8L8L8Q8Q8S$$//5 9T/''--##EIIk2$>?Csxxzuyy267 (2))d$@$GG"eiir:EIIiQS<TUUU #++ d$:DAG",UYYy"-EFc599Z#8999 #
 g ## 	NN<=	" + A V :sl   J6I> C J69J,:4J6.J//C6J6%J2&AJ66J47J6>(J)&J6(J))J6/J62J64J6c                    || _         y)zFRegister the callback for connector->gateway interrupt_inbound frames.N)r  r   s     r   set_interrupt_inbound_handlerz5WebSocketRelayTransport.set_interrupt_inbound_handler  s
    *1'r   c                    || _         y)uL  Register the callback for connector->gateway passthrough_forward frames.

        Mirrors set_interrupt_inbound_handler: the runner/adapter wires this so a
        forwarded passthrough request (Phase 5 §5.1) reaches the adapter over the
        same outbound WS the gateway already holds. ``handler(forward, buffer_id)``.
        N)r  r   s     r   set_passthrough_handlerz/WebSocketRelayTransport.set_passthrough_handler  s     %,!r   )r   r'   r2   r'   rN   r'   ri   zOptional[list[tuple[str, str]]]rj   floatrk   r$  rl   Optional[str]rm   r%  rn   boolro   r$  rp   r$  returnNone)r'  r&  )r'  r(  )r'  zDict[str, str])r'  r   )r   r   r'  r(  )r   Dict[str, Any]r2   r%  r'  r)  )r2   r%  r'  r%  )r4   r'   r'  r)  r   )r   r'   r   r%  r'  r(  )g      $@)r   r$  r'  r&  )r   r'   r'  r(  )r   )r   r)  r   r'   r2   r%  r'  r)  )r   r)  r'  r(  )r  BaseExceptionr'  zOptional[int])r  r'   r'  r(  )r   r   r'  r(  ) rS   rT   rU   rV   _HANDSHAKE_TIMEOUT_S_OUTBOUND_TIMEOUT_Sr   r   r   r   r   r   propertyr   r   r   r   r   r   r   r   r   r   r   r   r   staticmethodr   r   r   r!  r#  rX   r   r   rg   rg      s   X 7;#7$7$((,%()-[#[# [# 	[# 4[# ![# "[# "[# &[# [# #[# "'[# 
[#|W44"W<_ " " 
 DHG$G3@G	G DHG$G3@G	G"S^(2*X
M %0
 #'00 0
  0 
0D6
,\ 7 7J@6p2,r   rg   )r   r'   r'  r'   )r)   r   r'  r%  )r   r)  r'  r	   )r   r)  r'  rM   )&rV   
__future__r   r   r   loggingr   dataclassesr   typingr   r   r   r   gateway.platforms.baser	   r
   gateway.sessionr   gateway.relay.descriptorr   gateway.relay.transportr   	getLoggerrS   r   r   ImportErrorrr   r+  r,  r   r   r/   rK   rM   re   rg   rX   r   r   <module>r9     s   : #     ! , , < ) 9 2			8	$ "-     "& :#0LDN   "<B, B,W  Js   B BB