
    `gj                        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y)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_sc                   g ddit        j                         d fd}t        j                  |dd      }|j                          j	                  |      }|s3t
        j                  d|t        d	 D                     t                nt                d   r't
        j                  d
t        d D                     dj                        j                  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.
    	truncatedFc                    d} 	 j                         D ]c  }|s| z
  }|dk  rdd<    nOt        |      |kD  r j                  |d |        | |z  } dd<    n!j                  |       | t        |      z  } e j                          y # t        $ r }t        j                  d|       Y d }~5d }~ww xY w# j                          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   responsestates	       I/root/.hermes/venv/lib/python3.12/site-packages/agent/bounded_response.py_drainz)read_streaming_error_body.<locals>._drainN   s    	!,,. $%-	>)-E+&u:	)MM%
"34Y&E)-E+&e$U#$" HHJ  	DLL=sCC	D HHJs*   A6B 	B5B0+B8 0B55B8 8C
zbounded-error-body-readT)targetnamedaemon)timeoutzCbounded error-body read: hard timeout after %.1fs (%d bytes so far)c              3  2   K   | ]  }t        |        y w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  2   K   | ]  }t        |        y wr"   r#   r$   s     r   r'   z,read_streaming_error_body.<locals>.<genexpr>z   r(   r)       zutf-8replace)errors)returnNone)	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   s    $ F% E??D , 5dF LLNyyy+HQ'''	
 	HH[B'''	

 88F""79"==r+   c                D    	 | j                          y # t        $ r Y y w xY wr"   )closer   )r   s    r   r6   r6      s#     s    	c               *    t        | ||      }|xs 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_defaultr@      s!     %ID <4r+   )r   httpx.Responser   intr   floatr.   str)r   rA   r.   r/   )r   rA   r   rB   r   rC   r.   zOptional[str])__doc__
__future__r   loggingr0   typingr   r   httpx	getLogger__name__r   DEFAULT_ERROR_BODY_MAX_BYTESDEFAULT_ERROR_BODY_TIMEOUT_Sr;   r6   r@    r+   r   <module>rO      s   "H #   ! 			8	$  )   $  23	E>E> E> 	E>
 	E>P 23	  	
 r+   