
    Jj                        d Z ddlmZ ddlZddlZddlmZmZ ddlZ ej	        e
          ZdZdZeedddZddZeedddZdS )ut  Bounded reads of HTTP error response bodies.

When a provider returns a non-OK status on a *streaming* request, Hermes reads
the response body to build a useful diagnostic error. A bare ``response.read()``
on a streaming httpx response is unbounded in two dangerous ways:

1. A server can declare (or stream) an arbitrarily large body, so the read can
   balloon memory.
2. A server can open the body and then stall forever (no ``Content-Length``,
   no further bytes), so the read hangs the agent indefinitely.

Both are realistic against a misbehaving proxy, a hijacked endpoint, or a
provider having a bad day. The diagnostic body is only ever shown to the user
truncated to a few hundred characters, so reading megabytes — or blocking
forever — buys nothing.

``read_streaming_error_body`` bounds the read to a byte cap and enforces a
hard wall-clock deadline, returning the decoded text snippet. Callers pass the
returned text into their existing error builders instead of touching
``response.text`` (which would be unbounded / would raise after a partial
stream read).

A subtlety the implementation must respect: ``httpx``'s ``iter_bytes()`` blocks
*inside* the C/socket read while waiting for the next chunk. A wall-clock check
placed only between yielded chunks cannot interrupt a server that opens the
body and then stalls mid-chunk — control never returns to Python until httpx's
own (often 30s+) read timeout fires. To guarantee a bounded stop regardless of
socket behavior, the read runs on a daemon worker thread and the caller waits
on it with a hard deadline; on timeout we close the response (which unblocks /
cancels the read) and return whatever partial bytes were collected.

Ported and adapted from openclaw/openclaw#95108 ("bound Anthropic error
streams"), generalized to cover Hermes's three streaming error-body sites
(native Gemini, Gemini Cloud Code, Antigravity Cloud Code).
    )annotationsN)ListOptionali   g      $@	max_bytes	timeout_sresponsehttpx.Responser   intr   floatreturnstrc               F    g ddit          j                    d fd}t          j        |dd          }|                                                     |	          }|sCt
                              d
|t          d D                                  t                      nt                      d         r3t
                              dt          d D                                  d	                              
                    dd          S )a  Read a non-OK streaming response body with a byte cap and a hard deadline.

    Returns the decoded body text (UTF-8, errors replaced), truncated to
    ``max_bytes``. Never raises: any transport error, stall, or oversize
    condition is swallowed and the best-effort partial text (or an empty
    string) is returned, because this runs on the error path and must not
    mask the original HTTP failure with a read error.

    The byte cap protects against huge bodies; the wall-clock deadline (enforced
    via a worker thread so it can interrupt a socket read that stalls mid-chunk)
    protects against bodies that open and then hang.
    	truncatedFr   Nonec                    d} 	                                  D ]z}|s| z
  }|dk    rdd<    ndt          |          |k    r)                    |d |                    | |z  } dd<    n(                    |           | t          |          z  } {n2# t          $ r%}t                              d|           Y d }~nd }~ww xY w                                 d S #                                  w xY w)Nr   Tr   z"bounded error-body read failed: %s)
iter_byteslenappend	Exceptionloggerdebugset)	totalchunk	remainingexcchunksdoner   r	   states	       </home/rurouni/.hermes/hermes-agent/agent/bounded_response.py_drainz)read_streaming_error_body.<locals>._drainN   s+   	!,,.. $ $ %-	>>)-E+&Eu::	))MM%

"3444Y&E)-E+&Ee$$$U# 	D 	D 	DLL=sCCCCCCCC	D HHJJJJJDHHJJJJs0   BB C 
CB?:C ?CC C3zbounded-error-body-readT)targetnamedaemon)timeoutzCbounded error-body read: hard timeout after %.1fs (%d bytes so far)c              3  4   K   | ]}t          |          V  d S Nr   .0cs     r!   	<genexpr>z,read_streaming_error_body.<locals>.<genexpr>n   (      ''1A''''''    z4bounded error-body read: capped at %d bytes (max=%d)c              3  4   K   | ]}t          |          V  d S r(   r)   r*   s     r!   r-   z,read_streaming_error_body.<locals>.<genexpr>z   r.   r/   r/   zutf-8replace)errors)r   r   )	threadingEventThreadstartwaitr   r   sum_safe_closejoindecode)	r	   r   r   r"   workerfinishedr   r   r    s	   ``    @@@r!   read_streaming_error_bodyr>   8   sc   $ F% E?D         , 5d  F LLNNNyyy++H Q'''''''	
 	
 	
 	HH[ 
B'''''''	
 	
 	

 88F""79"===r/   r   c                R    	 |                                   d S # t          $ r Y d S w xY wr(   )closer   )r	   s    r!   r9   r9      s?       s    
&&Optional[str]c               .    t          | ||          }|pdS )zLike ``read_streaming_error_body`` but returns ``None`` on empty body.

    Convenience for callers that distinguish "no body" from "empty string".
    r   N)r>   )r	   r   r   texts       r!   read_error_body_or_defaultrD      s+     %I  D <4r/   )r	   r
   r   r   r   r   r   r   )r	   r
   r   r   )r	   r
   r   r   r   r   r   rA   )__doc__
__future__r   loggingr3   typingr   r   httpx	getLogger__name__r   DEFAULT_ERROR_BODY_MAX_BYTESDEFAULT_ERROR_BODY_TIMEOUT_Sr>   r9   rD    r/   r!   <module>rO      s   " "H # " " " " "      ! ! ! ! ! ! ! ! 		8	$	$  )   $  23	E> E> E> E> E> E>P    23	       r/   