
    @=j                    x    d Z ddlmZ ddlZddlmZmZmZ dZdZ	h dZ
d#d$dZe	fd%dZd&dZd'dZd(dZd)d"ZdS )*uH  Scale-to-zero idle detection + dormant-quiesce for the gateway (Phase 0).

This is the gateway-side BEHAVIOUR layer that consumes the relay scale-to-zero
PRIMITIVES (gateway-gateway Phase 5: the buffered-flip, the durable per-instance
buffer, the wakeUrl poke, the reconnect supervisor). It owns the *decision* to go
idle and drives the relay transport's ``go_dormant()`` (D12) — it does NOT itself
suspend the machine. On Fly, the now-traffic-idle machine is suspended by
``autostop:"suspend"`` and woken by autostart-on-wakeUrl (decisions.md Q3=C′).

Design constraints (decisions.md):
  - Per-instance enable is gated SOLELY by the NAS "Labs" toggle, carried to the
    gateway as the ``HERMES_SCALE_TO_ZERO`` env stamp (D11/Q8=A). NOT a user
    config key; ``scale_to_zero.idle_timeout_minutes`` IS config.yaml (D2).
  - Arm only when messaging is relay-only or absent (D1/F6) AND a wakeUrl is
    registered (§3.4(1)) AND the flag is set.
  - Idle = no in-flight agent turn AND no inbound for N min AND no live
    background work (D2/D3/F7).
  - The quiesce uses ``go_dormant()`` (socket closed + supervisor preserved),
    NEVER the stop/restart drain or ``disconnect()`` (F12/F14). The process stays
    alive; Fly freezes+resumes it.
  - ``mark_resume_pending`` is deliberately NOT called here (D13 — suspend
    preserves RAM; revive only if we move to autostop:"stop" or see kills).

The pure helpers (``parse_idle_timeout_seconds``, ``scale_to_zero_enabled``,
``messaging_is_relay_only_or_absent``, ``is_idle``, ``should_arm``) take plain
inputs so they unit-test without a live gateway.
    )annotationsN)AnyIterableOptionalHERMES_SCALE_TO_ZERO   >   1onyestrueenvironOptional[dict]returnboolc                    | | nt           j        }t          |                    t          d                                                                                    t          v S )zWhether the per-instance Labs toggle is on (the HERMES_SCALE_TO_ZERO stamp).

    D11/Q8=A: this env flag is the SOLE per-instance enable signal reaching the
    gateway. Absent/blank/falsey -> disabled (fail-safe default off).
    N )osr   strgetSCALE_TO_ZERO_ENVstriplower_TRUTHY)r   envs     ;/home/rurouni/.hermes/hermes-agent/gateway/scale_to_zero.pyscale_to_zero_enabledr   ,   sN     (''bjCsww("--..4466<<>>'II    	cfg_valuer   default_minutesintfloatc                    	 t          |           }n&# t          t          f$ r t          |          }Y nw xY w|dk    rt          |          }|dz  S )u$  Coerce ``scale_to_zero.idle_timeout_minutes`` (config.yaml, D2) to seconds.

    Degrades to the default on any non-numeric / non-positive value (never raises,
    never returns <= 0 — a zero/negative timeout would make the gateway go dormant
    instantly, which is never the intent).
    r   g      N@)r!   	TypeError
ValueError)r   r   minutess      r   parse_idle_timeout_secondsr&   6   sg    )	""z" ) ) )(()!||((T>s     55	platformsIterable[Any]c                j    d | D             }|                     d           t          |          dk    S )u  True iff the only connected messaging platform is RELAY, or there is none
    (a Chronos-only / no-platform agent) — the F6/D1 structural precondition.

    A directly-connected platform (Discord/Telegram/Slack/...) holds a live
    socket and cannot scale to zero, so its presence disarms the feature. We
    compare by the platform's ``.value``/name to avoid importing the enum here
    (keeps this module import-light and unit-testable).
    c                ,    h | ]}t          |          S  )_platform_name).0ps     r   	<setcomp>z4messaging_is_relay_only_or_absent.<locals>.<setcomp>Q   s     2221^A222r   relayr   )discardlen)r'   namess     r   !messaging_is_relay_only_or_absentr4   H   s9     32	222E	MM'u::?r   platformr   c                    t          | d|           }t          |                                                                          S )Nvalue)getattrr   r   r   )r5   r7   s     r   r,   r,   V   s7    Hgx00Eu::##%%%r   enabledrelay_only_or_absentwake_urlOptional[str]c                \    t          |           ot          |          ot          |          S )uz  Whether to start the idle watcher at all (D1/D11/§3.4(1)).

    ALL must hold: the Labs flag is on, messaging is relay-only/absent, and a
    wakeUrl is registered (a suspended instance with no reachable wake target is
    a black hole — §3.4(1)). Any unmet -> the watcher never starts (no idle
    timer, no dormancy), so a non-opted instance behaves exactly as today.
    )r   )r9   r:   r;   s      r   
should_armr>   [   s)     ==JT"677JDNNJr   running_agent_countseconds_since_last_inboundidle_timeout_secondshas_live_background_workc                &    | dk    rdS |rdS ||k    S )uB  The idle predicate (D2/D3/F7). Pure — composes the three conjuncts.

    Idle iff: no in-flight agent turn, no inbound within the timeout window, and
    no live background work (backgrounded delegate_task / kanban / bg terminal).
    Any active work keeps the gateway awake — suspending mid-flight would lose it.
    r   Fr+   )r?   r@   rA   rB   s       r   is_idlerD   k   s-     Qu u%)===r   )N)r   r   r   r   )r   r   r   r    r   r!   )r'   r(   r   r   )r5   r   r   r   )r9   r   r:   r   r;   r<   r   r   )
r?   r    r@   r!   rA   r!   rB   r   r   r   )__doc__
__future__r   r   typingr   r   r   r   DEFAULT_IDLE_TIMEOUT_MINUTESr   r   r&   r4   r,   r>   rD   r+   r   r   <module>rI      s    8 # " " " " " 				 * * * * * * * * * * +   ! 
$
$
$J J J J J ,H    $   & & & &
K K K K > > > > > >r   