
    `gjo9                    :   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mZm	Z	  ej                  e      ZdZdZdZdZd	Zd
Z G d de      Z G d de      Z ej,                  dej.                        ZddZddZddZddZ G d d      Zdaded<   ddZ ddZ!y) a3  
Signal attachment rate-limit scheduler.

Process-wide token-bucket simulator that mirrors the per-account
attachment rate limit signal-cli/Signal-Server enforce. Producers
(``SignalAdapter.send_multiple_images`` and the ``send_message`` tool's
Signal path) call ``acquire(n)`` before an attachment send; on a 429
they call ``feedback(retry_after, n)`` so the model recalibrates from
the server's authoritative hint.

The scheduler serializes concurrent calls through an ``asyncio.Lock``,
giving FIFO fairness across agent sessions sharing one signal-cli
daemon.
    )annotationsN)AnyOptional    2         g      $@c                  &     e Zd ZdZdd fdZ xZS )SignalRateLimitErroru(  
    Raised by ``SignalAdapter._rpc`` for rate-limit responses when the
    caller has opted in via ``raise_on_rate_limit=True``.

    Carries the server-supplied per-token Retry-After (in seconds) on
    signal-cli ≥ v0.14.3
    ``retry_after`` is None when the version doesn't expose it.
    c                2    t         |   |       || _        y N)super__init__retry_after)selfmessager   	__class__s      V/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/signal_rate_limit.pyr   zSignalRateLimitError.__init__5   s    !&    r   )r   strr   Optional[float]returnNone)__name__
__module____qualname____doc__r   __classcell__)r   s   @r   r   r   +   s    ' 'r   r   c                      e Zd Zy)SignalSchedulerErrorN)r   r   r    r   r   r!   r!   :   s    r   r!   z$Retry after (\d+(?:\.\d+)?)\s*secondc                   d}t        | t              r| j                  d      xs i }|j                  d      xs i }|j                  d      xs g }|D cg c]4  }t        |t              r"|j                  d      r|j                  d      6 }}|rt        t	        |            S t        | j                  dd            }nt        |       }t        j                  |      }|rt        |j                  d            S dS c c}w )	u2  Pull the per-token Retry-After window from a signal-cli rate-limit error.

    Tries two sources, in order:
    1. ``error.data.response.results[*].retryAfterSeconds`` — the
       structured field signal-cli ≥ v0.14.3 surfaces for plain
       RateLimitException.
    2. ``"Retry after N seconds"`` parsed out of the message — covers
       libsignal-net's RetryLaterException that gets wrapped as
       AttachmentInvalidException during attachment upload, where the
       structured field stays null.

    Returns None when neither yields a value.
     dataresponseresultsretryAfterSecondsr      N)	
isinstancedictgetfloatmaxr   _RETRY_AFTER_REsearchgroup)errmsgr%   r&   r'   r
candidatesmatchs           r   _extract_retry_after_secondsr7   J   s     C#twwv$"88J'-2,,y)/R07
+,!T"quu-@'A EE%&

 
 Z))#'')R()#h""3'E$)5Q 3t3
s   9Dc                   t        | t              r| j                  d      t        k(  ryt        | t              rt	        | j                  dd            n
t	        |       }|j                         }d|v xs d|v xs
 d|v xs d|v S )	u  True if a signal-cli RPC error reflects a rate-limit failure.

    Matches three layers:
    - typed ``RATELIMIT_ERROR`` code (signal-cli ≥ v0.14.3, plain
      RateLimitException)
    - legacy ``[429] / RateLimitException`` substrings
    - libsignal-net's ``RetryLaterException`` / ``Retry after N seconds``
      surfaced inside ``AttachmentInvalidException`` when the rate
      limit is hit during attachment upload — signal-cli never re-tags
      these as RateLimitException, so substring is the only signal.
    codeTr   r$   z[429]	ratelimitretrylaterexceptionzretry after)r*   r+   r,   SIGNAL_RPC_ERROR_RATELIMITr   lower)r2   r   	msg_lowers      r   _is_signal_rate_limit_errorr?   j   s     #t4N!N c4  	CGGIr"#X 
 I7 	&)#	& I-	& I%	r   c           	         t        d|       }|dk  rt        t        |             dS t        dt        t        |dz                     dS )z9Human-friendly wait label for user-facing pacing notices.        Z   sr)   <   z min)r.   intround)secondsrC   s     r   _format_waitrH      sL    CA2veAh-""!Sq2v'()..r   c                ,    | dk  ryt        dd| z        S )u  HTTP timeout for a Signal ``send`` RPC.

    signal-cli uploads attachments serially during the call, so the
    server-side time scales with batch size. Default 30s is fine for
    text-only sends but truncates large attachment batches mid-upload —
    we then log a phantom failure even though signal-cli completes the
    send a few seconds later. Scale at 5s/attachment with a 60s floor.
    r   g      >@g      N@g      @)r.   )num_attachmentss    r   _signal_send_timeoutrK      s!     !tS?*++r   c                  p    e Zd ZdZ ee       ee      f	 	 	 	 	 d
dZddZddZ	ddZ
ddZddZddZy	)SignalAttachmentScheduleru[  Process-wide token-bucket simulator for Signal attachment sends.

    The bucket holds up to ``capacity`` tokens (default 50, matching
    Signal's server-side rate-limit bucket size). Each attachment consumes one
    token. Tokens refill at ``refill_rate`` tokens/second, calibrated
    from the per-token Retry-After hint we get from the server when a
    429 fires. Until we've observed one, we use the documented default
    (1 token / 4 seconds).

    Concurrent ``acquire(n)`` calls serialize through an
    ``asyncio.Lock`` — natural FIFO across agent sessions hitting the
    same daemon.
    c                    t        |      | _        t        |      | _        dt        |      z  | _        t	        j
                         | _        t        j                         | _	        y )N      ?)
r-   capacitytokensrefill_ratetime	monotoniclast_refillasyncioLock_lock)r   rP   default_retry_afters      r   r   z"SignalAttachmentScheduler.__init__   sI    
 hHo':!;;>>+\\^
r   c                    t        j                         }|| j                  z
  }|dkD  rN| j                  | j                  k  r5t        | j                  | j                  || j                  z  z         | _        || _        y )Nr   rS   rT   rU   rQ   rP   minrR   )r   nowelapseds      r   _refillz!SignalAttachmentScheduler._refill   sb    nn(((Q;4;;6dmmT[[7TEUEU;U-UVDKr   c                   t        j                         }|| j                  z
  }| j                  }|dkD  r5|| j                  k  r&t        | j                  ||| j                  z  z         }||z
  }|dk  ry|| j                  z  S )aD  Best-effort estimate of the seconds until ``n`` tokens would
        be available. Used to decide whether to emit a user-facing
        pacing notice *before* committing to an ``acquire`` that may
        block silently. Lock-free; small races vs. concurrent acquires
        are benign for an informational notice.
        r   rA   r[   )r   nr]   r^   	projecteddeficits         r   estimate_waitz'SignalAttachmentScheduler.estimate_wait   s     nn(((KK	Q;9t}}4DMM9wAQAQ7Q+QRIi-a<))))r   c           
       K   |dk  ry|| j                   kD  rt        d| d| j                    d      d}d}	 | j                  4 d{    | j                          | j                  |k\  r<|r|dkD  r"t
        j                  d|| j                  |       |cddd      d{    S || j                  z
  }ddd      d{    | j                  z  }|r>t
        j                  d	||| j                  || j                  d
| j                  z         d}t        j                  |       d{    ||z  }7 7 7 {# 1 d{  7  sw Y   xY w7 &w)u  Block until at least ``n`` tokens are available, return the
        seconds slept.

        Does **not** deduct tokens — the bucket is a read-only model of
        server-side capacity.  Call ``report_rpc_duration()`` after the
        RPC to synchronise the model with the server timeline.

        Not perfect in case lots of coroutines try to acquire for big
        uploads (``report_rpc_duration`` will take a long time to get hit)
        but this is just a simulation. Signal server is ground truth and
        will raise rate-limit exceptions triggering requeues.

        The lock is released during ``asyncio.sleep`` so other callers
        can interleave.  A retry loop re-checks after each sleep in
        case the deadline was pessimistic.
        r   rA   z'Signal scheduler was called requesting z tokens (max is )TNzNSignal scheduler: tokens sufficient for %d (remaining=%.1f, total_slept=%.1fs)uk   Signal scheduler: pausing %.1fs for %d tokens (available=%.1f, deficit=%.1f, refill=%.4f/s ≈ %.1fs/token)rO   F)rP   r!   rX   r_   rQ   loggerdebugrR   inforV   sleep)r   ra   total_slept
first_passrc   waits         r   acquirez!SignalAttachmentScheduler.acquire   sh    " 6t}}&9! ===/, 
 
zz 
* 
*;;!#%qBt{{K
 '
* 
* 
* dkk/
* 
* T---DT!T[['$$cD,<,<&<	 #
--%%%4K- 
* 
* 
* 
* 
* 
*( &st   AE	D;
EA
EE#D=$E)E8ED?A+E/E0E=E?EEE
EEc           
       K   |dk  ry| j                   4 d{    t        j                         }| j                  }t	        d|t        |      z
        | _        || _        ddd      d{    t        j                  |dkD  r|dkD  rt        j                  nt        j                  d||| j                  || j                         y7 7 _# 1 d{  7  sw Y   oxY ww)u  Record an attachment-send RPC that just completed.

        Deducts ``n_attachments`` tokens without crediting refill during
        the upload window. Signal's server checks the bucket at RPC start
        and does *not* refill during request processing — refill resumes
        after the response. Crediting upload-time refill causes cumulative
        drift that eventually triggers 429s.

        Advances ``last_refill`` so the next ``acquire`` / ``_refill``
        starts counting from this point.
        r   NrA   
      u   Signal scheduler: RPC for %d att took %.1fs — tokens %.1f → %.1f (deducted=%d, no upload refill credited, refill=%.4fs⁻¹))rX   rS   rT   rQ   r.   r-   rU   rg   logloggingINFODEBUGrR   )r   rpc_durationn_attachmentsr]   token_befores        r   report_rpc_durationz-SignalAttachmentScheduler.report_rpc_duration  s      A:: 	# 	#.."C;;Lc<%2F#FGDK"D		# 	#
 	

(2--!2CGLL_<$++4++	
	# 	# 	# 	# 	#sF   C$CC$AC"C$-C.AC$C$C!CC!C$c                    |r@|dkD  r;dt        |      z  }|| j                  k7  rt        j                  d||       || _        d| _        t        j                         | _        y)a-  Apply server feedback after a 429.

        ``retry_after`` is the per-*token* refill window the server
        reports (None when signal-cli is older than v0.14.3 and didn't
        surface it).

        When present we calibrate ``refill_rate`` from it:
        the server is authoritative.
        r   rO   zaSignal scheduler: calibrating refill_rate to %.4f tokens/sec (server retry_after=%.1fs per token)rA   N)r-   rR   rg   ri   rQ   rS   rT   rU   )r   r   n_attemptednew_rates       r   feedbackz"SignalAttachmentScheduler.feedback/  s`     ;?U;//H4+++;k
 $, >>+r   c                   t        j                         }|| j                  z
  }| j                  }|dkD  r5|| j                  k  r&t        | j                  ||| j                  z  z         }t        |d      t        | j                        t        | j                  d      | j                  dkD  rt        d| j                  z  d      dS t        d      dS )u   Return current scheduler state for diagnostic logging (read-only).

        Does not advance ``last_refill`` — safe to call from logging paths
        without perturbing the bucket.
        r   r)   r   rO   inf)rQ   rP   rR   refill_seconds_per_token)
rS   rT   rU   rQ   rP   r\   rR   rF   rE   r-   )r   r]   r^   rb   s       r   statezSignalAttachmentScheduler.stateE  s     nn(((KK	Q;9t}}4DMM9wAQAQ7Q+QRIIq)DMM* !1!115LPL\L\_`L`cD4D4D.Da(H	
 	
 gllqfr	
 	
r   N)rP   r-   rY   r-   r   r   r   r   )ra   rE   r   r-   )rv   r-   rw   rE   r   r   )r   r   r{   rE   r   r   )r   r+   )r   r   r   r   r-   !SIGNAL_RATE_LIMIT_BUCKET_CAPACITY%SIGNAL_RATE_LIMIT_DEFAULT_RETRY_AFTERr   r_   rd   rn   ry   r}   r   r"   r   r   rM   rM      s[        AB%*+P%Q	$	$ #	$ 
		$*"1 f
:,,
r   rM   z#Optional[SignalAttachmentScheduler]
_schedulerc                     t         Xt               a t        j                  dt	        t         j
                        t         j                  dt         j                  z         t         S )z?Return the process-wide scheduler, creating it on first access.uM   Signal scheduler: created (capacity=%d tokens, refill=%.4f/s ≈ %.1fs/token)rO   )r   rM   rg   ri   rE   rP   rR   r"   r   r   get_schedulerr   _  sN     .0
[
##$""*(((		
 r   c                     da y)u   Drop the cached scheduler so the next ``get_scheduler`` call
    builds a fresh one. Test-only — never call from production paths.N)r   r"   r   r   _reset_schedulerr   m  s	     Jr   )r2   r   r   r   )r2   r   r   bool)rG   r-   r   r   )rJ   rE   r   r-   )r   rM   r   )"r   
__future__r   rV   rs   rerS   typingr   r   	getLoggerr   rg   SIGNAL_MAX_ATTACHMENTS_PER_MSGr   r   SIGNAL_RATE_LIMIT_MAX_ATTEMPTS$SIGNAL_BATCH_PACING_NOTICE_THRESHOLDr<   	Exceptionr   r!   compile
IGNORECASEr/   r7   r?   rH   rK   rM   r   __annotations__r   r   r"   r   r   <module>r      s    #   	   			8	$ "$ $& !() %!" '+ $ '9 '	9 	 "**DbmmT4@B/,$p
 p
n 37
/ 6r   