
    `gjC'                        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	m
Z
 ddZddZddZ G d d	      Zddd
Zg dZy)u  Synthetic GIL-heavy turn driver for the AC-4 isolation certify harness.

Mechanism B (the class ``docs/desktop/2026-07-04-dashboard-process-isolation-PRD.md``
targets) is interpreter-wide GIL starvation: concurrent heavy agent turns run
compute in threads of the SERVING process, and CPython's single GIL lets those
threads starve the event loop that flushes WebSocket frames for MINUTES. A
2026-07-04 ``sample`` showed the loop thread parked in ``take_gil`` while worker
threads burned the interpreter — NOT blocked on I/O.

To certify the fix (AC-4) without spending real tokens on 6 concurrent 100K+
context model calls, the harness needs a turn driver that reproduces THAT
regime: sustained pure-Python CPU that holds the GIL for the turn's duration.
A network/sleep stub is WRONG here — it would release the GIL during I/O and
never reproduce ``take_gil`` contention, so a dry-run green off it is a fake
green (the spec says so explicitly).

This module is a **test seam**: it is dead unless ``HERMES_ISO_CERTIFY_SYNTH_TURN``
is set. When armed, ``tui_gateway.server._make_agent`` returns a
:class:`SyntheticHeavyAgent` instead of a real ``AIAgent``. Because both the
in-process ``_pool`` path (isolation OFF) and the compute-host child path
(isolation ON) build their agent through ``_make_agent``, the SAME synthetic
turn exercises whichever dispatch path is under test — the isolation boundary
is the only variable between an OFF run and an ON run.

The per-turn intensity (wall duration, CPU chunk size, streamed-delta cadence,
token accounting) is carried in the prompt text as a small JSON spec so the
harness has full control and the server seam stays dumb. Any prompt that is not
a JSON object falls back to env / built-in defaults.
    )annotationsN)AnyCallableOptionalc                 F    t         j                  j                  d      dk(  S )z8True when the synthetic-turn test seam is armed via env.HERMES_ISO_CERTIFY_SYNTH_TURN1)osenvironget     M/root/.hermes/venv/lib/python3.12/site-packages/tui_gateway/synthetic_turn.pysynth_turn_armedr   (   s    ::>>9:cAAr   c                    	 t        t        j                  j                  | d      xs |      S # t        t
        f$ r |cY S w xY wN )floatr
   r   r   	TypeError
ValueErrornamedefaults     r   
_env_floatr   -   s?    RZZ^^D"-899z"    ,/ AAc                    	 t        t        j                  j                  | d      xs |      S # t        t
        f$ r |cY S w xY wr   )intr
   r   r   r   r   r   s     r   _env_intr   4   s?    2::>>$+6w77z" r   c                  x    e Zd ZdZddddZddZddZddZddZe	dd	       Z
d
d
d
d	 	 	 	 	 	 	 	 	 	 	 ddZy
)SyntheticHeavyAgentu  An AIAgent-shaped object whose turn is a GIL-holding CPU burn.

    Presents only the surface ``tui_gateway.server``'s turn path and status
    helpers read: ``run_conversation``/``interrupt``/``clear_interrupt`` plus a
    handful of ``model``/``provider``/``session_*`` attributes consumed by
    ``_get_usage`` and ``_session_info``. It never opens a socket or spawns a
    subprocess, so the only work it does is the deterministic Python loop below —
    exactly the ``take_gil`` regime under test.
    synthetic-heavymodelc               \   || _         || _        d| _        d| _        d| _        d| _        d| _        g | _        d | _        d | _	        d | _
        d| _        d| _        d| _        d| _        d| _        d| _        d| _        d| _        d| _        g | _        t+        j,                         | _        y )N	syntheticchat_completionsr   i@ r   )
session_idr#   providerapi_modebase_urlapi_keyplatformtoolsreasoning_configservice_tiercontext_compressor_config_context_length_cached_system_promptsession_input_tokenssession_output_tokenssession_prompt_tokenssession_completion_tokenssession_reasoning_tokenssession_total_tokenssession_api_callshistory	threadingEvent
_interrupt)selfr'   r#   s      r   __init__zSyntheticHeavyAgent.__init__F   s    $
#* "
-1(,"&&-#%'"$%!%&"%&")*&()%$%!!"-/#//+r   c                8    | j                   j                          y N)r=   clearr>   s    r   clear_interruptz#SyntheticHeavyAgent.clear_interrupt`   s    r   c                8    | j                   j                          y rA   r=   setrC   s    r   	interruptzSyntheticHeavyAgent.interruptc   s    r   c                     y)NTr   rC   s    r   _has_stream_consumersz)SyntheticHeavyAgent._has_stream_consumersf   s    r   c                8    | j                   j                          y)zENo-op teardown (session lifecycle calls agent.close() on some paths).NrF   rC   s    r   closezSyntheticHeavyAgent.closei   s    r   c                @   i }t        | t              rI| j                         }|j                  d      r(	 t	        j
                  |      }t        |t              r|}t        |j                  dt        dd                  t        |j                  dt        dd                  t        |j                  dt        d	d
                  t        |j                  dt        dd                  t        |j                  dd            dS # t        t        f$ r i }Y w xY w)N{
duration_sHERMES_ISO_CERTIFY_DURATION_Sg       @chunkHERMES_ISO_CERTIFY_CHUNKi N  delta_interval_sHERMES_ISO_CERTIFY_DELTA_Sg?tokens_per_deltaHERMES_ISO_CERTIFY_TPDi   sleep_s        )rO   rQ   rS   rU   rW   )
isinstancestrstrip
startswithjsonloadsdictr   r   r   r   r   r   r   )messagespectextparseds       r   _parse_speczSyntheticHeavyAgent._parse_specn   s    !gs#==?Ds#!ZZ-F!&$/%
  zBacf7g hi '84NPV+WXY !&dhh/A:NjlpCq&r s !$DHH-?JbdgAh$i j TXXi56!
 	
 #I. Ds   'D	 	DDN)conversation_historystream_callbacktask_idc          	     n   | j                  |      }t        d|d         }t        d|d         }t        d|d         }	t        d|d         }
t        d|d	         }t        ||n| j                        }t	        j
                         }|}d}d}d
}	 | j                  j                         rd}nt	        j
                         }||z
  |k\  rnt        |      D ]  }|dz  dz   dz  } |rt	        j                  |       ||z
  |	k\  rU|dz  }| xj                  |
z  c_
        | xj                  |
z  c_        | xj                  |
z  c_        | |d|dd       |}| xj                  dz  c_        d| d| j                   d| d|dz  d}g |dt        |      d d dd|d}|| _        |||d d dS )NrX   rO      rQ   gMbP?rS   r   rU   rW   FTiCB i90  l    z	synthtok-05d z[synthetic heavy turn] deltas=z out_tokens=z interrupted=z
 checksum=i  04xuser   )rolecontent	assistant)final_responsemessagesinterruptederrorlast_reasoning)rd   maxlistr:   time	monotonicr=   is_setrangesleepr4   r6   r8   r9   rZ   )r>   r`   re   rf   rg   _kwargsra   durationrQ   intervalrU   rW   base_historystart
last_deltaaccdeltasrt   now_finalrs   s                         r   run_conversationz$SyntheticHeavyAgent.run_conversation   s*    (sD./AtG}%ud#567q$'9":;c4	?+4H4T0Z^ZfZfg 
%%'".."CU{h& 5\ FY/3EEF

7#Z8+!**.>>*..2BB.))-==)".#is|1$=> 
+ . 	!# -VH 5445 6&-z#,s1CE 	


GTc(:;
 !U3

  # &"
 	
r   )r'   rZ   r#   rZ   returnNone)r   r   r   bool)r`   r   r   dict[str, Any])r`   r   re   zOptional[list[dict[str, str]]]rf   zOptional[Callable[[str], None]]rg   zOptional[str]r~   r   r   r   )__name__
__module____qualname____doc__r?   rD   rH   rJ   rL   staticmethodrd   r   r   r   r   r    r    ;   s     9J ,4 
 
 
F @D;?!%B
B
 =	B

 9B
 B
 B
 
B
r   r    c                    t               syd}t        |t              r |j                  d      rt	        |d         }nt        |t              r|r|}t        | |      S )zReturn a :class:`SyntheticHeavyAgent` when the seam is armed, else ``None``.

    ``model_override`` (dict or str) only influences the reported ``model`` label
    so status frames look plausible; it never changes the compute.
    Nr!   r#   r"   )r   rY   r_   r   rZ   r    )r'   model_overrider#   s      r   maybe_build_synthetic_agentr      sV     E.$'N,>,>w,GN7+,	NC	(^z77r   )r    r   r   r   )r   rZ   r   r   r   r   )r   rZ   r   r   r   r   rA   )r'   rZ   r   r   r   zSyntheticHeavyAgent | None)r   
__future__r   r]   r
   r;   ry   typingr   r   r   r   r   r   r    r   __all__r   r   r   <module>r      sJ   < #  	   * *B
U
 U
p8 r   