
    `gj&                        d Z ddlmZ ddlZddlZddlmZmZmZ ddl	m
Z
  ej                  d      Zdddd	Z G d
 de
      ZddZy)u"  Chronos — NAS-mediated managed cron provider (scale-to-zero).

Chronos (the Greek god of time, alongside Hermes) is the first non-default
``CronScheduler``. It lets a hosted gateway scale to zero while idle and still
fire cron jobs: instead of a 60s in-process ticker, it asks NAS to arm exactly
one external one-shot per job at that job's real next-fire time. NAS calls the
agent back at fire time over an authenticated webhook (``/api/cron/fire``); the
agent runs the job via the shared ``run_one_job`` body and re-arms the next
one-shot.

The external scheduler NAS uses is an internal NAS implementation detail —
Chronos names no vendor, holds no scheduler credentials, and speaks only to
NAS's ``agent-cron`` endpoints with the agent's existing Nous token.

Design constraints (see the plan's DQ-1):
  - start() arms all enabled jobs and RETURNS; it never blocks and never spawns
    a periodic wake. Between fires the machine is truly at zero.
  - reconcile runs only on a warm process (start / on_jobs_changed / piggybacked
    on a fire), never as a periodic wake of a sleeping machine.

Inert unless ``cron.provider: chronos``. ``resolve_cron_scheduler`` falls back
to the built-in if Chronos is unavailable, so cron never loses its trigger.

Wire contract: ``docs/chronos-managed-cron-contract.md``.
    )annotationsN)AnyDictOptional)CronSchedulerzcron.chronos )defaultc                X    	 ddl m}m}  | |       g|d| iS # t        $ r | cY S w xY w)z0Read a cron.chronos.* config value (no network).r   )cfg_getload_configr	   )hermes_cli.configr   r   	Exception)r	   keysr   r   s       Z/root/.hermes/venv/lib/python3.12/site-packages/plugins/cron_providers/chronos/__init__.py_cfgr   &   s5    :{}=t=W== s    ))c                       e Zd ZdZddZedd       ZddZddZd Z	ddZ
ddd	d
dZddZddZddZddZddZddZdddd fdZ xZS )ChronosCronSchedulerz$NAS-mediated external cron provider.c                R    i | _         t        j                         | _        d | _        y N)_armed	threadingLock_lock_clientselfs    r   __init__zChronosCronScheduler.__init__2   s      ')^^%
    c                     y)Nchronos r   s    r   namezChronosCronScheduler.name<   s    r   c                X    t        ddd      rt        ddd      sy| j                         S )uD  Config presence only — NO network.

        Chronos needs a portal base URL, the agent's own publicly-reachable
        callback URL (for NAS→agent fires), and a usable Nous token (the agent
        is logged into the portal). If any is missing, resolve_cron_scheduler
        falls back to the built-in ticker.
        cronr    
portal_urlcallback_urlF)r   _have_nous_tokenr   s    r   is_availablez!ChronosCronScheduler.is_available@   s.     VY5$vyR`:a$$&&r   c                z    	 ddl m}  |d      xs i }t        |j                  d            S # t        $ r Y yw xY w)u"  True if the agent has a Nous Portal login (no network call).

        Checks the stored auth state for a Nous access token — does NOT refresh
        or hit the network (is_available must stay offline). The actual
        refresh-aware token is resolved lazily at provision time.
        r   )get_provider_auth_statenousaccess_tokenF)hermes_cli.authr*   boolgetr   )r   r*   states      r   r'   z%ChronosCronScheduler._have_nous_tokenL   s?    	?+F39rE		.122 		s   +. 	::c                n    | j                   ddlm}  |t        ddd            | _         | j                   S )N   )NasCronClientr$   r    r%   )r   _nas_clientr3   r   )r   r3   s     r   _get_clientz ChronosCronScheduler._get_client\   s.    <<2(fi)NODL||r   c                6    t        t        ddd      xs d      S )Nr$   r    r&   r   )strr   r   s    r   _callback_urlz"ChronosCronScheduler._callback_urlb   s    4	>:@bAAr   N<   )adaptersloopintervalc                   | j                          	 | j                          y# t        $ r }t        j	                  d|       Y d}~yd}~ww xY w)u   Arm all enabled jobs via NAS, then RETURN immediately.

        Does NOT block and does NOT spawn a 60s wake (DQ-1) — that is the whole
        point of scale-to-zero. The machine wakes only on a NAS→agent fire.
        z$Chronos start() reconcile failed: %sN)recover_interrupted	reconciler   loggerwarning)r   
stop_eventr:   r;   r<   es         r   startzChronosCronScheduler.startg   sD     	  "	FNN 	FNNA1EE	Fs   # 	AAAc                     y r   r!   r   s    r   stopzChronosCronScheduler.stopw   s    r   c                ~    	 | j                          y# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)u   A job was created/updated/removed/paused/resumed — reconcile the NAS
        registry so the affected one-shot is (re-)armed or cancelled.z,Chronos on_jobs_changed reconcile failed: %sN)r?   r   r@   debug)r   rC   s     r   on_jobs_changedz$ChronosCronScheduler.on_jobs_changedz   s6    	LNN 	LLLGKK	Ls    	<7<c                   |d   }|j                  d      }|sy| d| }| j                         j                  ||| j                         |       | j                  5  || j
                  |<   ddd       y# 1 sw Y   yxY w)a  Ask NAS to arm exactly one one-shot at the job's next_run_at.

        The agent computes the time; NAS+its scheduler are the dumb executor.
        Idempotent per (job_id, fire_at) via dedup_key, so re-arming the same
        fire is a no-op NAS-side.
        idnext_run_atN:)job_idfire_atagent_callback_url	dedup_key)r/   r5   	provisionr8   r   r   )r   jobrN   rO   rQ   s        r   _arm_one_shotz"ChronosCronScheduler._arm_one_shot   s     T''-(hay)	$$#113	 	% 	
 ZZ 	*")DKK	* 	* 	*s   A77B c                D   	 | j                         j                  |       | j                  5  | j                  j	                  |d        d d d        y # 1 sw Y   y xY w# | j                  5  | j                  j	                  |d        d d d        w # 1 sw Y   w xY wxY w)N)rN   )r5   cancelr   r   pop)r   rN   s     r   _cancelzChronosCronScheduler._cancel   s|    	.%%V%4 .-. . . .-. . .s.    A  AA B-B
	BBBc                    | j                   5  | j                  rt        | j                        cddd       S 	 ddd       	 | j                         j	                         D ci c])  }|j                  d      r|d   |j                  dd      + }}| j                   5  | j                  j                  |       ddd       |S # 1 sw Y   xY wc c}w # 1 sw Y   |S xY w# t        $ r"}t        j                  d|       i cY d}~S d}~ww xY w)u  Observed armed one-shots: job_id → fire_at.

        Prefer the in-memory map (warm process); on a cold/empty map, ask NAS
        (best-effort). If NAS list fails, return what we have — reconcile then
        re-arms desired jobs idempotently.
        NrN   rO   r   z9Chronos _list_armed failed (will re-arm idempotently): %s)
r   r   dictr5   
list_armedr/   updater   r@   rH   )r   itemobservedrC   s       r   _list_armedz ChronosCronScheduler._list_armed   s     ZZ 	){{DKK(	) 	)	)	 !,,.99;88H% XB 77H 
  -""8,-O	) 	)
-O 	LLTVWXI	sY   !C C" ".CC" C:	C" CC" CC" C" "	D+DDDc                   ddl m}  |       D ci c]A  }|j                  d      r.|j                  d      r|j                  d      dk7  r	|d   |d   C }}| j                         }|j	                         D ]=  \  }}|j                  |      |k7  sddl m}  ||      }|s,	 | j                  |       ? t        |j                               D ]  }||vs	 | j                  |        y
c c}w # t        $ r!}	t        j                  d	||	       Y d
}	~	d
}	~	ww xY w# t        $ r!}	t        j                  d||	       Y d
}	~	td
}	~	ww xY w)z~Converge the NAS-armed one-shots toward jobs.json (desired state):
        arm missing / re-arm changed-time, cancel orphaned.r   )	load_jobsenabledrL   r0   pausedrK   get_jobz Chronos failed to arm job %s: %sNz&Chronos failed to cancel orphan %s: %s)	cron.jobsra   r/   r_   itemsre   rT   r   r@   rA   listr   rX   )
r   ra   jdesiredr^   rN   rO   re   rS   rC   s
             r   r?   zChronosCronScheduler.reconcile   sK    	( [#
uuYAEE-$8QUU7^x=W dGQ}%%#
 #

 ##%  '}} 		VOFG||F#w.-foV**3/		V 8==?+ 	XFW$XLL(	X)#
  % V'I6STUUV ! XNN#KVUVWWXs6   AC.%C3D 3	D<DD 	E
)EE
r:   r;   c                  t         |   |||      }|rFddlm}  ||      }|r6|j	                  d      r%|j	                  d      r	 | j                  |       |S |S # t        $ r"}t        j                  d||       Y d}~|S d}~ww xY w)ua  Run the due job (claim + run_one_job via the ABC default), then
        re-arm the NEXT one-shot through NAS.

        Re-arm happens AFTER the run so next_run_at reflects the completed fire.
        If the job is gone (one-shot completed / repeat-N exhausted), get_job
        returns None → nothing to re-arm (the schedule naturally stops).
        rk   r   rd   rb   rL   z.Chronos failed to re-arm job %s after fire: %sN)	superfire_duerf   re   r/   rT   r   r@   rA   )	r   rN   r:   r;   ranre   rS   rC   	__class__s	           r   rn   zChronosCronScheduler.fire_due   s     gvtD)&/Cswwy)cggm.D`&&s+ 
s
 ! `NN#SU[]^__
`s   	A 	B	'BB	returnNone)rr   r7   )rr   r.   )rS   zDict[str, Any]rr   rs   )rN   r7   rr   rs   )rr   zDict[str, str])rN   r7   r:   r   r;   r   rr   r.   )__name__
__module____qualname____doc__r   propertyr"   r(   r'   r5   r8   rD   rF   rI   rT   rX   r_   r?   rn   __classcell__)rp   s   @r   r   r   /   su    .  
' B
 -1tb F L**.2XD 8<  r   r   c                6    | j                  t                      y)u   Plugin entrypoint — register the Chronos provider with the loader.

    Mirrors the memory-plugin shape; plugins/cron_providers discovery calls this and
    collects the provider via register_cron_scheduler.
    N)register_cron_schedulerr   )ctxs    r   registerr}      s      4 67r   )r   r7   r	   r   rr   r   rq   )rw   
__future__r   loggingr   typingr   r   r   cron.scheduler_providerr   	getLoggerr@   r   r   r}   r!   r   r   <module>r      sN   4 #   & & 1			>	* %' }= }@8r   