
    `gj                        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y)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truec                    | | nt         j                  }t        |j                  t        d            j                         j                         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).
     )osenvironstrgetSCALE_TO_ZERO_ENVstriplower_TRUTHY)r   envs     H/root/.hermes/venv/lib/python3.12/site-packages/gateway/scale_to_zero.pyscale_to_zero_enabledr   ,   sB     ('bjjCsww("-.446<<>'II    c                    	 t        |       }|dk  rt        |      }|dz  S # t        t        f$ r t        |      }Y 1w xY w)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@)float	TypeError
ValueError)	cfg_valuedefault_minutesminutess      r   parse_idle_timeout_secondsr"   6   sN    )	" !|(T>	 z" )()s   " ??c                z    | D ch c]  }t        |       }}|j                  d       t        |      dk(  S c c}w )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).
    relayr   )_platform_namediscardlen)	platformspnamess      r   !messaging_is_relay_only_or_absentr+   H   s;     )221^A2E2	MM'u:? 3s   8c                j    t        | d|       }t        |      j                         j                         S )Nvalue)getattrr   r   r   )platformr-   s     r   r%   r%   V   s,    Hgx0Eu:##%%r   c                L    t        |       xr t        |      xr 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.
    )bool)enabledrelay_only_or_absentwake_urls      r   
should_armr5   [   s"     =JT"67JDNJr   c                    | dkD  ry|ry||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   F )running_agent_countseconds_since_last_inboundidle_timeout_secondshas_live_background_works       r   is_idler<   k   s!     Q%)===r   )N)r   zOptional[dict]returnr1   )r   r   r    intr=   r   )r(   zIterable[Any]r=   r1   )r/   r   r=   r   )r2   r1   r3   r1   r4   zOptional[str]r=   r1   )
r8   r>   r9   r   r:   r   r;   r1   r=   r1   )__doc__
__future__r   r   typingr   r   r   r   DEFAULT_IDLE_TIMEOUT_MINUTESr   r   r"   r+   r%   r5   r<   r7   r   r   <module>rC      s   8 # 	 * * +   ! 
$J ,H%(
$&
KK K 	K
 
K >> !&>  	>
 #> 
>r   