
    sj&|             -          U d Z ddlZddlZddlm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ZddlZddlZddlZddlZ	 ddlZn# e$ r dZY nw xY w	 ddlZn# e$ r dZY nw xY wddlmZmZ ddlmZ ddlmZ ddlmZmZmZmZmZmZm Z m!Z!  ej"        e#          Z$dd	l%m&Z' dd
l(m)Z)m*Z* da+da,ee-         e.d<   de-fdZ/ e            0                                Z1e1dz  Z2e2dz  Z3e2dz  Z4e2dz  Z5dZ6 ej7                    Z8 ej9                    Z:i Z;ee<ej7        f         e.d<    ej=                    Z>dZ?e2dz  Z@dZA ed           G d d                      ZB edd          ZCeeeB                  e.d<    eBe2e3e@          ZDdeBfdZEejF        de e<ef         fd             ZGdefd!ZHd"ZId#ZJd$ZKdeLfd%ZMd&e<de-fd'ZNdefd(ZOejF        d)             ZPejF        d&e<fd*            ZQejF        d&e<d+e<fd,            ZR eSd-h          ZTd&e<defd.ZUdd/ee<         d0ee         dee<         fd1ZVd2ee<ef         dee<ef         fd3ZWdd5ed6e<de<fd7ZXd2ee<ef         de<fd8ZYd2ee<ef         dee<ef         fd9ZZd2ee<ef         de-fd:Z[d2ee<ef         de-fd;Z\d2ee<ef         de<fd<Z]d=efd>Z^d=efd?Z_d=ed@eej`                 ddfdAZadB ZbdCe<decfdDZddEe<dee<ef         fdFZedGedefdHZfdIedJede-fdKZgdIedJede-fdLZhddMdEee<ef         dNedOee<         dee<         fdPZidEejdecfdQZkdalece.dR<   dSZmg Zneoe.dT<   d2ee<ef         dEee<ef         dNede-fdUZpdEee<ef         deeL         fdVZqi Zree<eeL         f         e.dW<   d2ee<ef         dXe<ddfdYZsdee<ef         fdZZtddEee<ef         dOee<         dee<         fd[Zud=eddfd\Zvd=ed5ecddfd]Zwdd_e-ddfd`Zxd=edeeL         fdaZydeeL         fdbZzdeeL         fdcZ{dddZ|dee<ddfdfZ}decfdgZ~ddhZdee<         fdiZdjedeee-f         fdkZdeee<ef                  fdlZdeeee<ef                           fdmZdjedeeecececf                  fdnZdoeeecececf                  ddfdpZddqdreee<ef                  dsee!e<                  deee<ef                  fdtZdd^dudreee<ef                  dsee!e<                  dve-fdwZdd^dudreee<ef                  dsee!e<                  dve-fdxZdyee<         dee<         fdzZdee<         fd{Zd^d|d5ed}e-dee<         fd~Zd5edee<         fdZdededededeee<         ee<         f         f
dZd2ee<ef         deee<         ee<         ee<         e-f         fdZdee<         dee<         de-dee<         ddf
dZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddee<         dEe<dee<         deec         dee<         deee<ef                  d/ee<         d0eee<                  dee<         dee<         dee<         dee<         dee e<ee<         f                  deee<                  dyee<         de-dee-         dee<         dee<         dee<         dee<ef         f*dZd&e<deee<ef                  fdZ G d de          Zde<deee<ef                  fdZdde-deee<ef                  fdZd&e<dee<ef         deee<ef                  fdZdd&e<dee<         deee<ef                  fdZd&e<deee<ef                  fdZd&e<deee<ef                  fdZd&e<de-fdZ	 	 	 dddd&e<d_e-dee<         dee<         dee<         dee<         de-fdZd&e<de<d5e-de-fdZd&e<d5e-de-fdZd&e<de-fdZd&e<ddfdZd&e<de-fdZd&e<ddfdZd&e<de<de-fdZ	 	 ddddd&e<d_e-dee<         dee<         dee<         dee<         de-fdZd2ee<ef         ddfdZd&e<de-fdZd&e<d+e<de-fdZd&e<de-fdZdecfdZd&e<de-fdZde<fdZdd^d^dd&e<decde-de-de e-ee<ef         f         f
dZdd^d^dd&e<decde-de-de e-ee<ef         f         f
dZdZdeLfdZddqdeee<ef                  dNedseee<                  de-fdĄZd&e<d+e<de-fdńZd&e<d+e<de-fdƄZdeee<ef                  fdǄZdeee<ef                  fdȄZdZdecfdʄZdedecdecfd̈́Zd&e<de<fd΄Zdede<fdЄZdee<         fdфZ	 	 ddeee<e<f                  deee<                  dee<ef         fdԄZdS )z
Cron job storage and management.

Jobs are stored in ~/.hermes/cron/jobs.json
Output is saved to ~/.hermes/cron/output/{job_id}/{timestamp}.md
    N)
ContextVar)	dataclass)datetime	timedelta)Path)get_hermes_home)OptionalDictListAnySetTupleUnion
Collection)now)atomic_replaceatomic_write_textHAS_CRONITERreturnc                  t    t           	 ddlm}  | ada n# t          $ r da Y nw xY wt          t                     S )zDImport croniter on first use; honor a pre-set HAS_CRONITER override.Nr   )croniterTF)r   r   ImportErrorbool)	_croniters    -/home/agent/.hermes/hermes-agent/cron/jobs.py_ensure_croniterr   4   s_     	!666666 HLL 	! 	! 	! LLL	!s   
 ##cron	jobs.jsonticker_heartbeatticker_last_success<   _fire_fence_locksg      >@outputx   T)frozenc                   .    e Zd ZU eed<   eed<   eed<   dS )_CronStorePathscron_dir	jobs_file
output_dirN)__name__
__module____qualname__r   __annotations__     r   r'   r'   y   s0         NNNOOOr0   r'   cron_store_override)default_cron_store_overridec                  (   t                                           } | | S t          t          t          t
                    }|t          k    r|S t                                                      }|t          k    r|S |dz  }t          ||dz  |dz            S )u  Return paths pinned to this execution context's profile.

    Precedence, most explicit first:

    1. an active use_cron_store() override (ContextVar);
    2. deliberately re-pointed module constants — if CRON_DIR/JOBS_FILE/
       OUTPUT_DIR no longer match their import-time values, someone chose
       the documented process-wide compatibility surface; honor it;
    3. the ACTIVE profile home, resolved fresh via get_hermes_home()
       (context-local override, then the HERMES_HOME env var) — so a test
       or embedder that re-points HERMES_HOME after this module was
       imported reads/writes ITS OWN store, not whatever jobs.json the
       import happened to freeze (the filed incident: fixtures that patched
       the env too late silently rewrote the user's real jobs file);
    4. the import-time constants (home unchanged since import — the common
       path, returned unchanged).
    Nr   r   r#   )
r3   getr'   CRON_DIR	JOBS_FILE
OUTPUT_DIR_IMPORT_STOREr   resolve
HERMES_DIR)overridelive_constantshomer(   s       r   _current_cron_storer?      s    $ $''))H$Xy*EEN&&$$&&Dzf}H8X%;X=PQQQr0   r>   c              #   R  K   t          |                                                                           dz  }t                              t          ||dz  |dz                      }	 dV  t                              |           dS # t                              |           w xY w)z@Route cron storage to ``home`` without mutating process globals.r   r   r#   )r(   r)   r*   N)r   
expanduserr:   r3   setr'   reset)r>   r(   tokens      r   use_cron_storerE      s       Dzz$$&&..0069H $$,(*	
 	
 	
 E*""5)))))""5))))s   *B
 
B&c                  (    t                      j        S )z>Return the output directory for the active cron store context.)r?   r*   r/   r0   r   get_cron_output_dirrG      s      ++r0   i     g     @c                  H   t          j        dd                                          } t          }| r/	 t	          |           }n# t
          t          f$ r
 t          }Y nw xY w|dk    rt	          t                    S t          |t          z  t	          t                              S )ug  Resolve the one-shot running-claim stale-recovery TTL.

    Derived from ``HERMES_CRON_TIMEOUT`` (the cron inactivity timeout the
    scheduler enforces on each run) so the safety valve tracks how long a run
    is actually allowed to go quiet, instead of a magic constant:

    - unset / invalid → default 600s inactivity limit → TTL = 1800s
    - ``0`` (unlimited runs) → no finite bound to derive from → fall back to
      ``ONESHOT_RUN_CLAIM_TTL_SECONDS``
    - positive N → ``max(N * headroom, ONESHOT_RUN_CLAIM_TTL_SECONDS)`` so a
      tiny configured timeout can never expire a claim mid-run.
    HERMES_CRON_TIMEOUT r   )
osgetenvstrip _DEFAULT_CRON_INACTIVITY_TIMEOUTfloat
ValueError	TypeErrorONESHOT_RUN_CLAIM_TTL_SECONDSmax_ONESHOT_RUN_CLAIM_TTL_HEADROOM)rawtimeouts     r   _oneshot_run_claim_ttl_secondsrX      s     ))2
.
.
4
4
6
6C.G
 7	7CjjGGI& 	7 	7 	76GGG	7!||233311+,,  s   A AAjob_idc                     	 ddl m} |  |            v S # t          $ r! t                              d| d           Y dS w xY w)u|  Return True when the scheduler in THIS process is still running ``job_id``.

    Direct liveness signal for stale-entry recovery (#62002): the run_claim
    TTL alone cannot distinguish "the claiming tick died" from "the run is
    alive but slow" — a run stalled on network I/O (or a laptop that slept
    mid-run) legitimately outlives the TTL. The in-process ticker and the run
    share this process, so the scheduler's running set settles the common
    single-gateway case without any claim-age guesswork.

    Imported lazily: the scheduler imports this module at load, so a
    module-level import here would be circular.
    r   )get_running_job_idszsCron running-set liveness check failed for job %r; keeping the entry to avoid deleting a possibly live one-shot runTexc_info)cron.schedulerr[   	Exceptionloggerwarning)rY   r[   s     r   _job_running_in_this_processrb      sy    
666666,,....   C	 	 	
 	
 	
 tts    '??c                  .    t                      j        dz  S )z=Return the advisory lock path for the current cron directory.z
.jobs.lock)r?   r(   r/   r0   r   _jobs_lock_filerd     s      )L88r0   c            
   #     K   t          t          dd          } | rF| dz   t          _        	 dV  t          xj        dz  c_        n# t          xj        dz  c_        w xY wdS t          5  dt          _        dt          _        d}	 	 t                       t          t                      dd          }|                    d           t          t          j                    t          z   }	 	 t          j        |t          j        t          j        z             n# t           t"          f$ r t          j                    |k    rWt$                              d	t          t                                 	 |                                 n# t           $ r Y nw xY wd}Y nt          j        d
           Y nw xY wnLt,          E t          t,          d          |                                t          t,          d          d           n9# t           t"          f$ r%}t$                              d|           Y d}~nd}~ww xY w	 dV  |	 t           t          j        |t          j                   nLt,          E t          t,          d          |                                t          t,          d          d           n# t           t"          f$ r Y nw xY w|                                 n# |                                 w xY wn# |	 t           t          j        |t          j                   nLt,          E t          t,          d          |                                t          t,          d          d           n# t           t"          f$ r Y nw xY w|                                 w # |                                 w xY ww xY wdt          _        dt          _        n# dt          _        dt          _        w xY w	 ddd           dS # 1 swxY w Y   dS )u  Serialize a load_jobs→modify→save_jobs critical section.

    Combines the in-process threading lock (cheap mutual exclusion between
    the gateway's parallel tick threads) with a cross-process advisory file
    lock on ``<cron dir>/.jobs.lock`` (mutual exclusion between the gateway process
    and standalone ``hermes`` CLI invocations, which previously shared no lock
    at all — a `cron pause` could be silently clobbered by a concurrent
    gateway write, leaving a "paused" job still firing).

    The flock is blocking, but every critical section that uses it is short
    (field updates only — no agent execution), so contention resolves in
    milliseconds. If neither fcntl nor msvcrt is available the manager still
    provides in-process locking, matching the historical behaviour.

    Nested calls in the same thread reuse the held lock so legacy callers that
    invoke save_jobs() inside a broader mutation section don't deadlock or try
    to reacquire the advisory file lock.
    depthr      Na+utf-8encodingTu   Timed out after %.0fs waiting for the cron jobs lock (%s) — another process is holding it. Proceeding with in-process locking only so the scheduler stays alive (#60703).皙?lockingLK_LOCKzSjobs.json cross-process lock unavailable (%s); proceeding with in-process lock onlyLK_UNLCK)getattr_jobs_lock_staterf   _jobs_file_lock
load_stampensure_dirsopenrd   seekfcntltime	monotonic_JOBS_LOCK_TIMEOUT_SECONDSflockLOCK_EXLOCK_NBOSErrorIOErrorr`   errorclosesleepmsvcrtfilenora   LOCK_UN)rf   lock_fd	_deadlinees       r   
_jobs_lockr     s     ( $gq11E !&	(EEE""a'"""""a'""""""	 I/ I/!" '+#@	//J00$IIIQ$ !% 0 03M MI,,!K1NOOO! '1 , , ,#~//9<< &%M %?$3$5$5!" !" !"!)$+MMOOOO'. !) !) !)$(D!)*. % JsOOOOO!,	,* '.GFI..w~~/?/?QZA[A[]^___W% J J J   FGHJ J J J J J J JJ
(&( ,!K????#/6GFI66w~~7G7GQWYcIdIdfghhh#W-      '7&( ,!K????#/6GFI66w~~7G7GQWYcIdIdfghhh#W-      ' &'"*.'' &'"*.'....'SI/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/ I/s6  A A(O?A$G?*,DG?AF--FF-
FF-FF-G?F-*G?,F--AG?>O?H5H0+O0H55O9K:=O A3J43K 4KK KK O K66O:N8>A3M21N2N	NN	N	N8N4	4N88O;O?O..O??PPc           
   #     K   t                      j        }|                                 d|  }t          5  t                              |t          j                              }ddd           n# 1 swxY w Y   |5  t                       t          j
        t          j        |          j        }|d| dz  }d}d}	 t          |dd          }|                    d	           t          t!          j                    t$          z   }	 	 t          j        |t          j        t          j        z             d
}n`# t,          t.          f$ rK t!          j                    |k    rt0                              d|           Y nt!          j        d           Y nw xY wnit6          H t9          t6          d          |                                t9          t6          d          d           d
}nt0                              d           n:# t,          t.          f$ r&}	t0                              d| |	           Y d}	~	nd}	~	ww xY w	 |V  |	 |r't           t          j        |t          j                   nN|rLt6          E t9          t6          d          |                                t9          t6          d          d           n# t,          t.          f$ r Y nw xY w|                                 n# |                                 w xY wn# |	 |r't           t          j        |t          j                   nN|rLt6          E t9          t6          d          |                                t9          t6          d          d           n# t,          t.          f$ r Y nw xY w|                                 w # |                                 w xY ww xY wddd           dS # 1 swxY w Y   dS )a(  Serialize one job's owner mutations and external side effects.

    Unlike the global jobs lock, this lock may be held across network delivery.
    It is scoped to one profile + job, so unrelated cron jobs keep progressing.
    Fencing fails closed when cross-process locking is unavailable.
    ::Nz.fire-z.lockFrh   ri   rj   r   Tz3Timed out waiting for fire fence %s; failing closedrl   rm   rn   rg   z1No cross-process lock backend for cron fire fencez&Cron fire fence unavailable for %s: %sro   ) r?   r(   r:   _fire_fence_locks_guardr"   
setdefault	threadingRLockrt   uuiduuid5NAMESPACE_URLhexru   rv   rw   rx   ry   rz   r{   r|   r}   r~   r   r`   r   r   r   rp   r   r   r   )
rY   r(   lock_key
local_lock	lock_name	lock_pathr   acquireddeadlineexcs
             r   _fire_job_lockr   y  s      #$$-H""$$0000H	  O O&11(IO<M<MNN
O O O O O O O O O O O O O O O 
 0$ 0$Jt18<<@	8	8888		P9dW===GLLOOO >++.HH((GU]U]-JKKK#'#W- ( ( (>++x77"LL U )   "E
3(( #*	**NN$$gfi&@&@!    PQQQ! 	P 	P 	PLLA63OOOOOOOO	P	$NNN"
$ E$5GU];;;;! f&82	22#NN,,gfj.I.I1    )   D MMOOOOGMMOOOO #w"
$ E$5GU];;;;! f&82	22#NN,,gfj.I.I1    )   D MMOOOOGMMOOOO #K0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$s  -A//A36A3<A O=A
H.D76H7AF:H<FHFA.HOH9H4/O4H99O=LOA7J<;K(<KK(KK(O(K>>OOA7M>=N*>N	N*N	N*O*O 	 OOOOexpected_ownerc             #      K   t                     5 }|sdV  	 ddd           dS t                      5  t           fdt                      D             d          }t	          |t
                    r|                    d          nd}t	          |t
                    o|                    d          |k    }ddd           n# 1 swxY w Y   |V  ddd           dS # 1 swxY w Y   dS )zEHold a per-job fence while an owner performs an external side effect.FNc              3   N   K   | ]}|                     d           k    |V   dS idNr5   ).0itemrY   s     r   	<genexpr>z#fire_claim_fence.<locals>.<genexpr>  s7      QQ&8P8P8P8P8P8PQQr0   
fire_claimby)r   r   next	load_jobs
isinstancedictr5   )rY   r   r   jobclaim
owns_claims   `     r   fire_claim_fencer     s      
		 
8 	KKK
 
 
 
 
 
 
 
 \\ 	 	QQQQQQQSWXXC-7T-B-BLCGGL)))E5$''MEIIdOO~,M 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s:   C!C!BC9C!C			C!C		C!!C%(C%r   c                 B   t          | pd                                          }|r|dv sd|v sd|v rt          d|           t          |                                          st          |          j        rt          d|           t                      j        |z  S )ad  Resolve a job's output directory, rejecting any path-escape attempt.

    Job IDs are filesystem path components under ``OUTPUT_DIR``. A legacy or
    crafted ID containing ``..``, absolute paths, or nested separators would
    allow output writes/deletes to escape the cron output sandbox. Reject
    anything that isn't a single safe path component.
    rK   >   .../\z%Invalid cron job id for output path: )strrN   rQ   r   is_absolutedriver?   r*   )rY   texts     r   _job_output_dirr     s     v|""$$D M4;&&#++KKKLLLDzz M4::#3 MKKKLLL  +d22r0   skillskillsc                     || r| gng }n(t          |t                    r|g}nt          |          }g }|D ]@}t          |pd                                          }|r||vr|                    |           A|S )zPNormalize legacy/single-skill and multi-skill inputs into a unique ordered list.NrK   )r   r   listrN   append)r   r   	raw_items
normalizedr   r   s         r   _normalize_skill_listr     s    ~$,UGG"			FC	 	  !H		LL	J $ $4:2$$&& 	$D
**d###r0   r   c                     t          |           }t          |                    d          |                    d                    }||d<   |r|d         nd|d<   |S )zLReturn a job dict with canonical `skills` and legacy `skill` fields aligned.r   r   r   N)r   r   r5   )r   r   r   s      r   _apply_skill_fieldsr     s[    cJ":>>'#:#:JNN8<T<TUUF!Jx'-7&))4Jwr0   rK   valuefallbackc                 (    | |S t          |           S )zFCoerce legacy/hand-edited nullable cron fields to strings for readers.r   )r   r   s     r   _coerce_job_textr     s    }u::r0   c                 l   t          |                     d                                                    }|r|S |                     d          }t          |t                    r@dD ]<}t          |                    |                                                    }|r|c S =n|t          |          S dS )Nschedule_displayschedule)displayr   exprrun_at?)r   r5   rN   r   r   r   )r   r   r   keyr   s        r   _schedule_display_for_jobr      s    sww'9::;;AACCG wwz""H(D!! 9 	 	C#HLL$5$566<<>>D 	 
	8}}3r0   c                 ~   t          |           }t          |                    d          d          }t          |                    d                    }||d<   ||d<   t          |                    d                                                    }|st          |                    d                                                    }|p*|                    d          r|d         d         ndp|p|pd	}|d
d                                         pd	}||d<   t	          |          |d<   t          |          |d<   |S )a,  Return a read-safe cron job shape for UI/API/tool/scheduler consumers.

    Older or hand-edited jobs can have nullable fields like ``prompt``,
    ``name``, or ``schedule_display``.  Keep storage untouched on read, but
    ensure consumers never crash while formatting or running those records.
    r   unknownpromptnamescriptr   r   rK   cron jobN2   r   state)r   r   r5   rN   r   effective_job_state)r   r   rY   r   r   r   label_sources          r   _normalize_job_recordr     sO    %S))JjnnT22I>>FjnnX6677FJt!JxJNN6223399;;D 	7!*..":":;;AACC +5>>(+C+CK
8$Q''   	 CRC &&((6JJv%>z%J%JJ!"
 .j99Jwr0   c                     t          |                     d                                                    dk    rdS t          |                     d                    S )z>True when the record carries any operator-facing pause signal.r   pausedT	paused_at)r   r5   rN   r   r   s    r   _has_pause_markerr   4  sL    (())//11X==t$$%%%r0   c                 X    |                      dd          sdS t          |           rdS dS )zTrue iff the scheduler may fire this job.

    ``enabled`` is the scheduler-honoured flag. Pause markers (``state`` /
    ``paused_at``) are a second gate so a contradictory half-paused record
    never fires even before self-heal runs.
    enabledTF)r5   r   r   s    r   is_job_runnabler   ;  s9     779d## u u4r0   c                    t          |                     d                                                    }|dv r|S |                     dd          st          |           s|dk    rdS |pdS |dk    s|                     d          rdS |pdS )u&  Operator-facing state derived from the scheduler-honoured flag.

    A job with ``enabled=true`` must never display as paused — that was the
    07-30 outage failure mode (list looked frozen, fleet kept merging).
    Terminal states (completed/error) are preserved regardless of enabled.
    r   >   r   	completedr   Tr   r   	scheduled)r   r5   rN   r   )r   storeds     r   r   r   I  s     cggg..//5577F'''779d## "S!! 	Vx%7%78!!SWW[11{ [ r0   pathc                 b    	 t          j        | d           dS # t          t          f$ r Y dS w xY w)z<Set directory to owner-only access (0700). No-op on Windows.i  N)rL   chmodr~   NotImplementedErrorr   s    r   _secure_dirr   ]  sG    
u()   s    ..c                     	 |                                  rt          j        | d           dS dS # t          t          f$ r Y dS w xY w)z;Set file to owner-only read/write (0600). No-op on Windows.i  N)existsrL   r   r~   r   r   s    r   _secure_filer   e  sa    ;;== 	"HT5!!!!!	" 	"()   s   )/ AAbeforec                    |t           j        dk    rdS t          t           dd          }t          t           dd          }||dS 	  |            }|dk    rdS |j        |j        f| |            fk    rdS t          j        | |j        |j                   dS # t          $ r3}t                              d| |j        |j        |           Y d}~dS d}~ww xY w)u  Restore a rewritten file's previous owner (POSIX, privileged writer only).

    The atomic-write pattern (mkstemp + replace) makes the rewritten file owned
    by the *writer's* euid. When a root shell runs a state-writing cron CLI
    command (``docker exec hermes hermes cron create ...`` — ``docker exec``
    defaults to root) against a store owned by the unprivileged gateway user,
    the replace flips ``jobs.json`` to ``root:root`` mode 600 and the gateway's
    ticker (uid 1000) is silently locked out of every subsequent tick (#68483).

    Root can always hand ownership back, so do exactly that: when the euid is 0
    and the pre-replace owner differs, chown the new file to the previous
    uid/gid. Unprivileged writers are a no-op (their own rewrite already heals
    a root-owned file back to their uid, and they couldn't chown anyway).
    No-op on Windows. Best-effort: a failure must never break the save.
    Nposixgeteuidgetegidr   u   Could not restore ownership of %s to uid=%s gid=%s after rewrite: %s — if the gateway runs as a different user, its cron ticker may now be locked out (see issue #68483).)	rL   r   rp   st_uidst_gidchownr~   r`   ra   )r   r   r   r   euidr   s         r   _preserve_file_ownershipr   n  s     ~G++b)T**Gb)T**G'/
wyy199FM6=)dGGII->>>F
v}fm44444 
 
 
0 &-		
 	
 	
 	
 	
 	
 	
 	
 	

s$   B B 8 B 
C$(CCc                      t                      } | j                            dd           | j                            dd           t	          | j                   t	          | j                   dS )z6Ensure cron directories exist with secure permissions.Tparentsexist_okN)r?   r(   mkdirr*   r   stores    r   rt   rt     sk    !!E	N555	4$777 !!!!!r0   sc                 >   |                                                                  } t          j        d|           }|st	          d|  d          t          |                    d                    }|                    d          d         }dddd	}|||         z  S )
u   
    Parse duration string into minutes.
    
    Examples:
        "30m" → 30
        "2h" → 120
        "1d" → 1440
    zD^(\d+)\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$zInvalid duration: 'z''. Use format like '30m', '2h', or '1d'rg      r   r!   i  )mhd)rN   lowerrematchrQ   intgroup)r  r
  r   unitmultiplierss        r   parse_durationr    s     	
		AH\^_``E [YqYYYZZZAE;;q>>!D..K;t$$$r0   r   c                    |                                  } | }|                                 }|                    d          r5| dd                                          }t          |          }d|d| ddS |                                 }t          |          dk    r|t          d |dd         D                       r[t                      st          d	          	 t          |            n'# t          $ r}t          d
|  d|           d}~ww xY wd| | dS d| v st          j        d|           r	 t          j        |                     dd                    }|j        )t#                      j        }|                    |          }d|                                d|                    d           dS # t          $ r}t          d|  d|           d}~ww xY w	 t          |           }t#                      t)          |          z   }	d|	                                d| dS # t          $ r Y nw xY wt          d| d          )uM  
    Parse schedule string into structured format.
    
    Returns dict with:
        - kind: "once" | "interval" | "cron"
        - For "once": "run_at" (ISO timestamp)
        - For "interval": "minutes" (int)
        - For "cron": "expr" (cron expression)
    
    Examples:
        "30m"              → once in 30 minutes
        "2h"               → once in 2 hours
        "every 30m"        → recurring every 30 minutes
        "every 2h"         → recurring every 2 hours
        "0 9 * * *"        → cron expression
        "2026-02-03T14:00" → once at timestamp
    zevery    Nintervalr  )kindminutesr      c              3   @   K   | ]}t          j        d |          V  dS )z^[\d\*\-,/]+$N)r	  r
  )r   ps     r   r   z!parse_schedule.<locals>.<genexpr>  s@        *+!1%%     r0   zOCron expressions require 'croniter' package. Install with: pip install croniterzInvalid cron expression 'z': r   )r  r   r   Tz^\d{4}-\d{2}-\d{2}Zz+00:00tzinfooncezonce at z%Y-%m-%d %H:%M)r  r   r   zInvalid timestamp 'r  zonce in zInvalid schedule 'z'. Use:
  - Duration: '30m', '2h', '1d' (one-shot)
  - Interval: 'every 30m', 'every 2h' (recurring)
  - Cron: '0 9 * * *' (cron expression)
  - Timestamp: '2026-02-03T14:00:00' (one-shot at time))rN   r  
startswithr  splitlenallr   rQ   r   r_   r	  r
  r   fromisoformatreplacer  _hermes_now	isoformatstrftimer   )
r   originalschedule_lowerduration_strr  partsr   dt	hermes_tzr   s
             r   parse_scheduler-    s   $ ~~HH^^%%N   ** 
|))++ ..****
 
 	
 NNE
5zzQ3  /4RaRy      !! 	pnooo	KX 	K 	K 	KIIIaIIJJJ	K 
 
 	
 h"(#8(CC	E'(8(8h(G(GHHB y 'MM0	ZZyZ11,,..Ebkk2B&C&CEE  
  	E 	E 	EC8CCCCDDD	E	 **7!;!;!;;&&((,(,,
 
 	

     	CX 	C 	C 	C  sD   C+ +
D5D

D2BF8 8
GGG AH) )
H65H6r+  c                    t                      j        }| j        St          j                                                    j        }|                     |                              |          S |                     |          S )a  Return a timezone-aware datetime in Hermes configured timezone.

    Backward compatibility:
    - Older stored timestamps may be naive.
    - Naive values are interpreted as *system-local wall time* (the timezone
      `datetime.now()` used when they were created), then converted to the
      configured Hermes timezone.

    This preserves relative ordering for legacy naive timestamps across
    timezone changes and avoids false not-due results.
    Nr  )r$  r  r   r   
astimezoner#  )r+  	target_tzlocal_tzs      r   _ensure_awarer2    sf     $I	y<>>,,..5zzz**55i@@@==###r0   r   currentc                 v    | j         |j         dS |                                 |                                k    S )u	  Return True when a stored aware timestamp uses a different UTC offset.

    Naive stored timestamps return False: they carry no offset to compare, and
    are normalized by ``_ensure_aware`` instead — they intentionally never take
    the offset-repair path.
    NF)r  	utcoffsetr   r3  s     r   _timezone_offset_mismatchr7  -  s;     } 6u!2!2!4!444r0   c                 ^    |                      d          |                     d          k    S )a  Return True when the stored local wall-clock time has not arrived yet.

    Cron schedules express local wall-clock intent. If Hermes/system local time
    changes after next_run_at was persisted, an old offset can make a future
    wall-clock run look due at the converted absolute time (for example
    21:00+10 becomes 13:00+02). Comparing naive wall-clock values lets us
    distinguish that migration case from a genuinely missed run whose scheduled
    wall time has already passed.
    Nr  )r#  r6  s     r   _stored_wall_clock_is_futurer9  9  s+     >>>&&)E)EEEr0   last_run_atr   r;  c                D   t          | t                    r|                     d          dk    rdS |rdS |                     d          }|sdS 	 t          t	          j        |                    }n# t          $ r Y dS w xY w||t          t                    z
  k    r|S dS )a  Return a one-shot run time if it is still eligible to fire.

    One-shot jobs get a small grace window so jobs created a few seconds after
    their requested minute still run on the next tick. Once a one-shot has
    already run, it is never eligible again.
    r  r  Nr   )seconds)	r   r   r5   r2  r   r"  r_   r   ONESHOT_GRACE_SECONDS)r   r   r;  r   	run_at_dts        r   _recoverable_oneshot_run_atr@  F  s     h%% f)=)=)G)Gt t\\(##F t!("8"@"@AA		   ttC),ABBBBBB4s   !A1 1
A?>A?c                     d}d}t          |           }|s|S t          |          dz  }t          |t          ||                    S )a  Compute how late a job can be and still catch up instead of fast-forwarding.

    Uses half the schedule period (via ``_schedule_cadence_seconds``, the
    single cadence-measurement implementation), clamped between 120 seconds
    and 2 hours.  This ensures daily jobs can catch up if missed by up to
    2 hours, while frequent jobs (every 5-10 min) still fast-forward quickly.
    r$   i   r  )_schedule_cadence_secondsr  rT   min)r   	MIN_GRACE	MAX_GRACEperiod_secondsgraces        r   _compute_grace_secondsrH  d  sU     II.x88N 1$Ey#eY//000r0   _persisted_error_recoveries   "_persisted_error_recoveries_recentc                    |                      d          dk    rdS t          t          |                      d          pd                    rdS |                      d          }|sdS 	 t          t	          j        |                    }n# t          t          f$ r Y dS w xY w||z
                                  }|dk     rdS t          |          }|t          |          }||t          |          z   k    S )u  True when a recurring job is wedged in a stale persisted error state.

    Condition (all must hold):
      * it is a recurring (cron/interval) job (checked by caller);
      * its persisted ``last_status == "error"`` (a prior fire errored and the
        job never recovered);
      * it has NOT successfully re-fired within its natural cadence — its
        ``last_run_at`` is older than ``cadence + grace``, so this is not a
        normal transient-error retry that will fire on its own soon, it is a
        job that has been sitting errored for a full period with no recovery;
      * it is not currently running in this process (a live run must never be
        re-armed underneath itself, #62002-style).

    ``last_run_at`` being older than one cadence is the key discriminator: a
    job that errors and is retried on its normal schedule keeps ``last_run_at``
    fresh (mark_job_run stamps it on every fire, success or failure), so the
    stale check does not fire for a job that is merely erroring-and-retrying.
    last_statusr   Fr   rK   r;  r   )r5   rb   r   r2  r   r"  rQ   rR   total_secondsrB  rH  )r   r   r   last_runlast_run_dtage_secondscadence_secondss          r   _job_is_stale_error_recurringrS    s   . ww}((u#C(;$<$<== uww}%%H u#H$:8$D$DEE	"   uu$3355KQu/99O 1::/,B8,L,LLMMs   )!B B B c                    t          | t                    sdS |                     d          }|dk    rD|                     d          }	 |rt          |          dz  ndS # t          t
          f$ r Y dS w xY w|dk    rt                      sdS |                     d          }|sdS |t          v rt          |         S 	 t                      }t          ||          }|
                    t                    }|
                    t                    }||z
                                  }|dk    r|nd}	n# t          $ r d}	Y nw xY wt          t                    d	k    rt                                           |	t          |<   |	S dS )
u  Approximate the natural period of a schedule, in seconds, or None.

    Interval jobs use ``minutes * 60``.  Cron jobs measure the gap between the
    next two fire times with croniter (falling back to None when croniter is
    missing or the expr is malformed).  Cron results are cached per expr —
    this runs inside ``_jobs_lock`` on every tick for every stale-errored
    job, and two croniter evaluations per call add up (the same reason
    ``scheduler.py`` caches ``_cron_interval_minutes``).  The measured gap
    can vary with the base time for irregular exprs; the cache trades that
    precision for not re-evaluating croniter under the lock, which is fine
    for a staleness *threshold*.
    Nr  r  r  g      N@r   r   r      )r   r   r5   rP   rR   rQ   r   _cron_cadence_cacher$  r   get_nextr   rN  r_   r   clear)
r   r  r  r   baseitfirstsecondgapresults
             r   rB  rB    s    h%% t<<Dz,,y))	,3=5>>D((=:& 	 	 	44	v~~!! 	4||F## 	4&&&&t,,	==D$%%BKK))E[[**FE>0022C!GGSSFF 	 	 	FFF	
 "##s**%%'''$*D!4s%   	A A43A4>A3D2 2E ErV  previous_next_runc                    t                      }|                     d          |                     d          p|                     d          ||                                d}t          dz  at                              |           t          dt           = 	 t                      j        dz  }|j	        
                    dd           t          |d	d
          5 }|                    t          j        |          dz              ddd           dS # 1 swxY w Y   dS # t          $ r&}t                               d|           Y d}~dS d}~ww xY w)zEPersist a countable, probe-visible signal for one stale-error re-arm.r   r   )rY   r   previous_next_run_at
rearmed_atrg   Nz persisted_error_recoveries.jsonlTr   ari   rj   
z4Could not append persisted-error-recovery record: %s)r$  r5   r%  rI  rK  r   !_PERSISTED_ERROR_RECOVERY_HISTORYr?   r(   parentr   ru   writejsondumpsr_   r`   debug)r   r_  r   entryr   fhr   s          r    _record_persisted_error_recoveryrm    s    --C''$--03774== 1mmoo	 E  1$&--e444*+N-N,N+NOR"$$-0RR$666$g... 	/"HHTZ&&-...	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ R R RKSQQQQQQQQQRs=   AD! +DD! DD! DD! !
E+EEc                  :    t           t          t                    dS )z5Probe-visible snapshot of persisted-error recoveries.)persisted_error_recoveriesrecent)rI  r   rK  r/   r0   r   "get_persisted_error_recovery_statsrq    s#     'B9::  r0   c                 v   t                      }t          | t                    sdS |                     d          }|dS |dk    rt	          | ||          S |dk    r|                     d          }|dS |rY	 t          t          j        |                    }|t          |          z   }n6# t          $ r |t          |          z   }Y nw xY w|t          |          z   }|
                                S |dk    r|                     d	          }|sdS t                      st                              d
|           dS |}|r5	 t          t          j        |                    }n# t          $ r |}Y nw xY wt          ||          }	|	                    t                    }|
                                S dS )zo
    Compute the next run time for a schedule.

    Returns ISO timestamp string, or None if no more runs.
    Nr  r  r:  r  r  r  r   r   zCannot compute next run for cron schedule %r: 'croniter' is not installed. croniter is a core dependency as of v0.9.x; reinstall hermes-agent or run 'pip install croniter' in your runtime env.)r$  r   r   r5   r@  r2  r   r"  r   r_   r%  r   r`   ra   r   rW  )
r   r;  r   r  r  lastnext_runr   	base_timer   s
             r   compute_next_runrv    s    --Ch%% t<<D|tv~~*8SkRRRR			,,y))?4 	8<$X%;K%H%HII)G"<"<"<< < < <7!;!;!;;< Yw7777H!!###	||F## 	4!! 	NN    4
 	 	  )(*@*M*MNN		      			 i((==**!!###4s$   94B. .CC!E) )E87E8c                     t                       t          | t          t          j                              d           dS )aR  Atomically write the current epoch time to ``path``.

    Delegates to :func:`utils.atomic_write_text` (tmpfile + fsync +
    ``atomic_replace``, same pattern as ``save_jobs``) so a concurrent reader
    in another process (``hermes cron status``) never sees a torn/truncated
    file. Best-effort: failures are swallowed by callers.
    z.hb_)
tmp_prefixN)rt   r   r   rx   r   s    r   _atomic_write_epochry  I  s6     MMMdC	,,@@@@@@r0   c           	      8   t                       t          j        t          | j                  dd          \  }}	 t          j        |dd          5 }|                    t          t          d|                               |	                                 t          j
        |                                           ddd           n# 1 swxY w Y   t          ||            dS # t          $ r( 	 t          j        |           n# t          $ r Y nw xY w w xY w)	z2Atomically persist a non-negative integer counter..tmpz.count_dirsuffixprefixwri   rj   r   N)rt   tempfilemkstempr   rf  rL   fdopenrg  rT   flushfsyncr   r   BaseExceptionunlinkr~   )r   r   fdtmp_pathfs        r   _atomic_write_counterr  U  sT   MMM#DK(8(8PYZZZLBYr3111 	!QGGCAu&&'''GGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	x&&&&&   	Ih 	 	 	D	sT   C' A+C
>C' 
CC' CC' '
D2DD
DDDDFsuccessc                     t                      }	 t          |j        dz             n# t          $ r Y nw xY w| r+	 t          |j        dz             dS # t          $ r Y dS w xY wdS )u?  Record a ticker liveness signal, and optionally a successful-tick signal.

    The ticker calls this once per loop iteration. ``success=True`` additionally
    bumps the *last successful tick* marker. We track two distinct signals so
    `hermes cron status` can tell a thread that is merely *alive and looping*
    (heartbeat fresh, success stale) from one that is actually *firing jobs*
    (both fresh) — a ticker stuck failing every tick would otherwise keep the
    plain heartbeat fresh and falsely report healthy (#32612, #32895).

    Resolution uses ``_current_cron_store()`` so the heartbeat is correctly
    scoped to the active profile's store — critical under multiplex_profiles
    where each profile needs its own liveness signal (#69377).

    Best-effort: a write failure must never disrupt the tick loop.
    r   r    N)r?   ry  r(   r_   )r  r  s     r   record_ticker_heartbeatr  g  s       !!EEN-??@@@@    	1F FGGGGG 	 	 	DD	 s   ( 
55A 
A"!A"c                     	 |                      d                                          }t          dt          j                    t	          |          z
            S # t
          $ r Y d S w xY w)Nri   rj   g        )	read_textrN   rT   rx   rP   r_   )r   rV   s     r   _epoch_file_ager    si    nngn..44663	eCjj0111   tts   AA 
A)(A)c                  L    t                      } t          | j        dz            S )u  Seconds since the ticker loop last iterated, or None if unknown.

    None = heartbeat file missing/unreadable (older build, never ran, or a
    torn read). Callers treat None as "cannot determine", not "dead".

    Resolution uses ``_current_cron_store()`` so the heartbeat is correctly
    scoped to the active profile — critical under multiplex_profiles where
    ``hermes cron status`` must report per-profile liveness (#69377).
    r   r?   r  r(   r   s    r   get_ticker_heartbeat_ager    s%      !!E5>,>>???r0   c                  L    t                      } t          | j        dz            S )u-  Seconds since the ticker last completed a tick WITHOUT raising, or None.

    Resolution uses ``_current_cron_store()`` so the heartbeat is correctly
    scoped to the active profile — critical under multiplex_profiles where
    ``hermes cron status`` must report per-profile liveness (#69377).
    r    r  r   s    r   get_ticker_success_ager    s%      !!E5>,AABBBr0   c                  8   t                      j        dz  } 	 	 t          |                     d                                                    }n# t
          t          f$ r d}Y nw xY wt          | t          d|          dz              dS # t          $ r Y dS w xY w)zIIncrement the profile-local stale-schedule catch-up counter, best effort.catch_up_occurrencesri   rj   r   rg   N)
r?   r(   r  r  rN   r~   rQ   r  rT   r_   )r   r   s     r   record_catch_up_occurrencer    s      ),BBD	88>>@@AAEE$ 	 	 	EEE	dC5MMA$566666   s/   5A B A%"B $A%%$B 
BBmessagec                    t                      }|j        dz  }	 t                       t          j        t          |j                  dd          \  }}	 t          j        |dd          5 }|	                    t          j
                     d|                                  d           |                                 t          j        |                                           d	d	d	           n# 1 swxY w Y   t          ||           d	S # t           $ r( 	 t          j        |           n# t$          $ r Y nw xY w w xY w# t&          $ r Y d	S w xY w)
aO  Persist the most recent tick failure so other processes can surface it.

    The ticker thread lives inside the gateway process; ``hermes cron
    status``/``list`` run in a separate process and previously could only
    infer "ticks may be failing" from marker staleness, with no clue WHY.
    A root-owned ``jobs.json`` (#68483) failed every tick for ~14h with the
    reason visible only in the gateway's errors.log. Writing the last error
    next to the heartbeat markers gives the CLI something concrete to show.

    Best-effort: a write failure must never disrupt the tick loop.
    ticker_last_errorr{  z.terr_r|  r  ri   rj   rd  N)r?   r(   rt   r  r  r   rf  rL   r  rg  rx   rN   r  r  r   r   r  r  r~   r_   )r  r  r   r  r  r  s         r   record_ticker_errorr    s     !!E>//D'DK  
 
 
H	2sW555 %49;;=='--//===>>>			$$$% % % % % % % % % % % % % % % 8T***** 	 	 		(####   	    sl   :E D ,A9C1%D 1C55D 8C59D 
E D.-E .
D;8E :D;;E  E 
EEc                      t                      j        dz  } 	 t          dt          |                     d                                                              S # t          t          f$ r Y dS w xY w)z7Return the profile-local stale-schedule catch-up count.r  r   ri   rj   )r?   r(   rT   r  r  rN   r~   rQ   r   s    r   get_catch_up_occurrence_countr    st      ),BBD1c$..'.::@@BBCCDDDZ    qqs   AA A0/A0c                  ~    t                      } 	 | j        dz                                   dS # t          $ r Y dS w xY w)zGRemove the last-tick-error marker after a successful tick. Best-effort.r  N)r?   r(   r  r~   r   s    r   clear_ticker_errorr    sT    !!E	-	-5577777   s   . 
<<c                  8   t                      } 	 | j        dz                      d          }n# t          $ r Y dS w xY w|                                }t          |          dk     rdS d                    |dd                                                   }|pdS )z<Return the most recent recorded tick error message, or None.r  ri   rj   Nr  rd  rg   )r?   r(   r  r_   
splitlinesr   joinrN   )r  rV   linesr  s       r   get_ticker_last_errorr    s    !!E~ 33>>>PP   ttNNE
5zzA~~tiiabb	""((**G?ds   / 
==r)   c                    t          | dd          5 }|                                }ddd           n# 1 swxY w Y   	 t          j        |          dfS # t          j        $ r t          j        |d          dfcY S w xY w)a  Tolerantly parse jobs.json; shared by load_jobs and the save-path peek.

    Returns ``(data, used_strict_fallback)``. utf-8-sig absorbs a Windows
    BOM; a strict parse failure is retried with ``strict=False`` to survive
    bare control characters in string values. IO errors from the open and
    parse errors from the fallback propagate to the caller, which decides
    between repair (load_jobs) and bail-out (peek).
    rz	utf-8-sigrj   NF)strictT)ru   readrh  loadsJSONDecodeError)r)   r  rV   s      r   _parse_jobs_filer    s     
i{	3	3	3 qffhh              3z#%% 3 3 3z#e,,,d22223s   488 A 'B ?B c                     t                      j        } t                       t          |           }|                                 st          d           g S 	 t          |           \  }}n{# t          $ r3}t          	                    d|           t          d|           |d}~wt          $ r3}t          	                    d|           t          d|           |d}~ww xY wt          |t                    rT|                    dg           }|r+|r)t          |           t                              d           t          |           |S t          |t"                    r<|r)t          |           t                              d           t          |           |S t          d	t%          |          j                   )
zLoad all jobs from storage.NzIOError reading jobs.json: %szFailed to read cron database: z#Failed to auto-repair jobs.json: %sz*Cron database corrupted and unrepairable: jobsz8Auto-repaired jobs.json (had invalid control characters)z3Auto-repaired jobs.json (bare list wrapped as dict)z7Cron database corrupted: expected {'jobs': [...]}, got )r?   r)   rt   _jobs_file_stampr   _record_load_stampr  r   r`   r   RuntimeErrorr_   r   r   r5   	save_jobsra   r   typer+   )r)   pre_read_stampdata_strict_retryr   r  s         r   r   r   	  s   #%%/IMMM &i00N 4   	T.y99mm H H H4a888?A??@@aG T T T:A>>>KKKLLRSST $ xx## 	WT 	WdOOONNUVVV>***$   	RdOOONNPQQQ>***
YDJJDWYY  s$   A* *
C"4.B""C"/.CC"c                  X   t                      j        } |                                 sg S 	 t          |           \  }}n# t          $ r Y dS w xY wt          |t                    r/|                    dg           }t          |t                    r|ndS t          |t                    r|S dS )u  Best-effort read of on-disk jobs without repair side-effects.

    Caller must hold ``_jobs_lock()``. Returns ``[]`` when the file is
    missing, ``None`` when the payload is unreadable/corrupt (caller should
    not attempt a shrink-merge against an unknown baseline). Never calls
    ``save_jobs`` — the repair-free property is what keeps the save path
    re-entrancy-safe (a repairing read here would recurse through
    ``_save_jobs_unlocked``).
    Nr  )	r?   r)   r   r  r_   r   r   r5   r   )r)   r  _r  s       r   _peek_jobs_unlockedr  8  s     $%%/I 	"9--aa   tt$ 8xx##!$--7tt47$ 4s   > 
AAc                 v    	 |                                  }|j        |j        |j        fS # t          $ r Y dS w xY w)u^  Cheap change-detection stamp for jobs.json: ``(mtime_ns, size, ino)``.

    ``None`` means the file is missing/unstatable. Used as a fast-path gate
    in front of the shrink-merge so the healthy no-race save costs one
    ``stat()`` instead of a full read+parse (the ``advance_next_runs``
    batching exists because this path is hot — see its docstring).
    ``st_ino`` is included because every legitimate writer goes through
    mkstemp+rename (new inode), so even a same-size write inside one mtime
    quantum on a coarse-clock filesystem (ext4 jiffies, network mounts)
    cannot false-match.
    N)statst_mtime_nsst_sizest_inor~   )r)   sts     r   r  r  Q  sK    ^^
BI66   tts   '* 
88stampc                 N    t          t          dd          sdS | t          _        dS )ue  Remember jobs.json's stamp for the enclosing _jobs_lock() section.

    No-op outside a critical section. Lets the save path skip the
    shrink-merge parse when the file provably hasn't changed since this
    section loaded it (#80703's fast-path). The caller must capture the
    stamp BEFORE reading the file: a sibling landing mid-read then leaves
    the recorded stamp OLDER than disk — a mismatch, so the merge runs
    (fail-safe direction). Stamping after the read would let that sibling's
    write be certified as "seen" without being in the loaded payload,
    wrongly suppressing the recovery.
    rf   r   N)rp   rq   rs   )r  s    r   r  r  d  s-     #Wa00 "'r0   removed_idsr  r  c                   t          t          dd          }|&t          t                      j                  |k    r| S t                      }|| S d |pdD             }t                      }| D ]T}t          |t                    r=|	                    d          r(|
                    t          |d                              Ug }|D ]r}t          |t                    s|	                    d          }	|	s0t          |	          }	|	|v s|	|v rH|                    |           |
                    |	           s|s| S t                              dt          |          d |D                        | |z   S )u  Return *jobs* plus any on-disk jobs missing from the save payload (#80624).

    Under ``_jobs_lock()``'s degraded flock-timeout path (#60703), two
    processes can both believe they own the store. A writer that loaded an
    older/smaller snapshot then calls ``save_jobs`` and would otherwise
    clobber concurrent creates (the filed ``no_agent`` watchdog pattern:
    CLI/tool create succeeds, then a gateway tick/remove rewrites
    ``jobs.json`` empty or without the new id).

    Intentional deletes pass ``removed_ids``. Any other id present on disk
    but absent from *jobs* is treated as a concurrent create and merged
    back before the atomic write. The caller's list is never mutated — a
    new list is returned when anything was recovered.

    Fast path: when the enclosing critical section recorded a load stamp
    and the file's ``(mtime_ns, size)`` still matches, nothing can have
    changed underneath us, so the read+parse is skipped entirely — one
    ``stat()`` on the healthy no-race save.
    rs   Nc                 0    h | ]}|t          |          S r/   r   r   is     r   	<setcomp>z._merge_unexpected_disk_jobs.<locals>.<setcomp>  s#    @@@!a@s1vv@@@r0   r/   r   zPreserved %d cron job(s) present on disk but missing from the in-memory save payload (concurrent create under degraded lock or stale writer) (#80624): %sc                 8    g | ]}|                     d           S r   r   r   js     r   
<listcomp>z/_merge_unexpected_disk_jobs.<locals>.<listcomp>  s"    (((t(((r0   )rp   rq   r  r?   r)   r  rB   r   r   r5   addr   r   r`   ra   r   )
r  r  r  	disk_jobsintended_removenew_idsr   	recovereddisk_jobdisk_ids
             r   _merge_unexpected_disk_jobsr  u  s   0 $lD99E-.A.C.C.MNNRWWW#%%I@@(9r@@@OG ( (c4   	(SWWT]] 	(KKCI'''&(I 
 
(D)) 	,,t$$ 	g,,gO!;!;"""G 
NN	( 	I((i(((   )r0   r  r#  r#  c                   t                      j        }t                       	 t          j        |          }n=# t
          $ r0 	 t          j        |j                  }n# t
          $ r d}Y nw xY wY nw xY wd}	 t          d          D ]}|st          | |          } t          j
        t          |j                  dd          \  }}	 t          j        |dd	          5 }t          j        | t                                                      d
|dd           |                                 t          j        |                                           ddd           n# 1 swxY w Y   n7# t(          $ r* 	 t          j        |           n# t
          $ r Y nw xY wd} w xY w|st-          t.          dd          }	|	duot1          |          |	k    }
|
rdnt3                      }|`d | D             d |pdD             t5          fd|D                       r*	 t          j        |           n# t
          $ r Y nw xY wd}t7          ||           d}t9          |           t;          ||           t=          d            dS |st          | |          } t          j
        t          |j                  dd          \  }}t          j        |dd	          5 }t          j        | t                                                      d
|dd           |                                 t          j        |                                           ddd           n# 1 swxY w Y   t7          ||           d}t9          |           t;          ||           dS # t(          $ r* |&	 t          j        |           n# t
          $ r Y nw xY w w xY w)a  Save all jobs to storage. Caller must hold _jobs_lock().

    ``removed_ids`` lists job ids this mutation intentionally deleted.
    ``replace=True`` skips the shrink-merge guard (tests / disaster recovery
    that mean to rewrite the store wholesale).
    Nr  r  r{  z.jobs_r|  r  ri   rj   )r  
updated_atr  F)indentensure_asciirs   c                     h | ]A}t          |t                    |                    d           ,t          |d                    BS r  r   r   r5   r   r  s     r   r  z&_save_jobs_unlocked.<locals>.<setcomp>  sV     # # #%a..# 4555;;#AdG# # #r0   c                 0    h | ]}|t          |          S r/   r   r  s     r   r  z&_save_jobs_unlocked.<locals>.<setcomp>   s#    III1qIAIIIr0   r/   c              3      K   | ]\}t          |t                    oB|                    d           o-t          |d                    vot          |d                    vV  ]dS r   r  )r   djintendedpayload_idss     r   r   z&_save_jobs_unlocked.<locals>.<genexpr>  s        
 	 #2t,, :FF4LL:4MM<:  4MM9	     r0   )r?   r)   rt   rL   r  r~   rf  ranger  r  r  r   r  rh  dumpr$  r%  r  r  r   r  r  rp   rq   r  r  anyr   r   r   r  )r  r  r#  r)   _stat_beforer  _attemptr  r  _stamp
_unchangedr  r  r  s               @@r   _save_jobs_unlockedr    s6    $%%/IMMM wy))      	 79#344LL 	  	  	 LLL	   H`a D	 D	H R24[QQQ#+	())&  LBYr3999 )QI!%[]]5L5L5N5NOO %*	    GGIIIHQXXZZ((() ) ) ) ) ) ) ) ) ) ) ) ) ) ) !   Ih''''   D  !
 !!1<FF$&P+;I+F+F&+P  %/IDD4G4I4I	(# #!%# # #K
  JI1BIIIH     
 #,     !!Ih////& ! ! ! D!#' 8Y///H###$Y=== t$$$FF  	N.tMMMD'I$%%fX
 
 
H Yr3111 	!QI[]]-D-D-F-FGG"	    GGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	x+++Y L99999   	(####   s  : 
A4AA4A.+A4-A..A43A4:AN  E/#A4E#E/#E'	'E/*E'	+E/.N  /
F#:FF#
FF#FF##A<N   H54N  5
I?N  IAN  AN  "A4M"N  "M&&N  )M&*4N   O-OO
OOOOc                x    t                      5  t          | ||           ddd           dS # 1 swxY w Y   dS )zSave all jobs to storage.

    See ``_save_jobs_unlocked`` for ``removed_ids`` / ``replace`` semantics
    (shrink-merge guard against concurrent-create clobber, #80624).
    r  N)r   r  )r  r  r#  s      r   r  r  :  s     
 L LDk7KKKKL L L L L L L L L L L L L L L L L Ls   /33workdirc                    | dS t          |                                           }|sdS t          |                                          }|                                st          d|d          |                                }|                                st          d|           |                                st          d|           t          |          S )uh  Normalize and validate a cron job workdir.

    Rules:
      - Empty / None → None (feature off, preserves old behaviour).
      - ``~`` is expanded.  Relative paths are rejected — cron jobs run detached
        from any shell cwd, so relative paths have no stable meaning.
      - The path must exist and be a directory at create/update time.  We do
        NOT re-check at run time (a user might briefly unmount the dir; the
        scheduler will just fall back to old behaviour with a logged warning).

    Returns the absolute path string, or None when disabled.
    Raises ValueError on invalid input.
    Nz+Cron workdir must be an absolute path (got zN). Cron jobs run detached from any shell cwd, so relative paths are ambiguous.zCron workdir does not exist: z!Cron workdir is not a directory: )	r   rN   r   rA   r   rQ   r:   r   is_dir)r  rV   expandedresolveds       r   _normalize_workdirr  I  s     t
g,,



C tCyy##%%H!! 
[# [ [ [
 
 	
 !!H?? ECCCDDD?? IGXGGHHHx==r0   c                  b   	 ddl m} m} t                      dz  }|                                sdS  ||          }	 ddlm} |                    |          }n# t          $ r Y nw xY w | |          }|	                    d          pi }t          |t                    rR|	                    d          }t          |t                    r(|                                r|                                S |	                    d          pi }t          |t                    r|                                pdS t          |t                    rU|	                    d          p|	                    d          }t          |t                    r|                                pdS dS # t          $ r Y dS w xY w)	uc  Resolve the global default model the same way the cron ticker does.

    Mirrors the unpinned-model resolution in ``cron/scheduler.py`` ``run_job``:
    read ``config.yaml`` ``model.default`` (or the ``model`` alias / bare string
    form), applying the managed-scope overlay and env expansion. Used by
    ``create_job`` to snapshot the default model for unpinned jobs so a later
    swap of the global default is detected at fire time (#44585).

    Returns the resolved model string, or ``None`` if config is missing/empty
    or resolution fails (fail-open — caller treats ``None`` as "no snapshot").
    r   )_expand_env_varsread_user_config_rawzconfig.yamlN)managed_scoper   modelr2   )hermes_cli.configr  r  r   r   
hermes_clir  apply_managed_overlayr_   r5   r   r   r   rN   )	r  r  cfg_pathcfgr  cron_cfg
cron_model	model_cfgr2   s	            r   _resolve_default_model_snapshotr  j  s   LLLLLLLL"$$}4   	4""8,,	00000055c::CC 	 	 	D	s## 776??(bh%% 	*!g..J*c** *z/?/?/A/A *!'')))GGG$$*	i%% 	-??$$,,i&& 	/mmI..H)--2H2HG'3'' /}}.$.t   ttsH   -F  F  A F  
A&#F  %A&&BF  2AF  4A)F   
F.-F.strip_trailing_slashr  c                    t          | t                    sd S |                                 }|r|                    d          }|pd S )Nr   )r   r   rN   rstrip)r   r  r   s      r   _normalize_job_optional_textr    sI    eS!! t;;==D  {{3<4r0   c                     | dS t          |                                                                           }|sdS ddlm}  ||          t          d| d          |dv rdS |S )u  Validate a per-job reasoning effort against the canonical grammar.

    Spelling-only validation at the storage choke point: the SAME parser
    every other effort surface uses (``hermes_constants.parse_reasoning_effort``)
    decides validity, so the cron knob can never be stricter or looser than
    its config.yaml sibling. Capability (whether the resolved model supports
    the level) is intentionally NOT checked here — the model is not knowable
    at create time (unpinned jobs, auth fallback), and the provider
    transports already clamp/omit at send time.

    Returns None for unset (None/empty string), the normalized lowercase
    level for valid input, and raises ValueError otherwise so nothing
    invalid ever persists for a fire-and-forget job.
    Nr   )parse_reasoning_effortzInvalid reasoning_effort zg. Valid levels: none, minimal, low, medium, high, xhigh, max, ultra (empty string clears the override).>   falsedisablednone)r   rN   r  hermes_constantsr  rQ   )r   r   r  s      r   _normalize_reasoning_effortr    s     }tu::##%%D t777777d##+2 2 2 2
 
 	
 $$$vKr0   providerr  base_urlno_agentc                    t          |           }t          |          }t          |d          }t          |          rdS d}d}|y	 ddlm}	 ddi}
|r||
d<    |	di |
}t	          |                    d	          pd
                                                                          }|pd}n# t          $ r d}Y nw xY w|$	 t                      pd}n# t          $ r d}Y nw xY w||fS )ap  Snapshot unpinned inference axes for the provider/model drift guard.

    Agent cron jobs with unpinned provider/model follow global config at fire
    time. Capture the current resolution for each unpinned axis so a later
    global switch fails closed instead of silently changing spend. Pinned axes
    and no-agent script jobs intentionally carry no snapshot.
    Tr   NNNr   )resolve_runtime_provider	requestedexplicit_base_urlr  rK   r/   )
r  r   hermes_cli.runtime_providerr  r   r5   rN   r  r_   r  )r  r  r  r  normalized_providernormalized_modelnormalized_base_urlprovider_snapshotmodel_snapshotr  runtime_kwargssnapsnap_providers                r   !_compute_provider_model_snapshotsr    sc    7x@@3E::6!   H~~ z'+$(N"
	%LLLLLL)40N" J6I23++==n==D 4 4 :;;AACCIIKKM - 5 	% 	% 	% $	%	"<>>F$NN 	" 	" 	"!NNN	"n,,s%   A%B. .B=<B=C C#"C#c                    t          |                     d                    t          |                     d                    t          |                     d          d          t          |                     d                    fS )zBReturn the stored inference-routing fields in their semantic form.r  r  r  Tr   r  )r  r5   r   r   s    r   _normalized_inference_axesr    so     	%SWWZ%8%899$SWWW%5%566$SWWZ%8%8tTTTSWWZ  !!	 r0   monitor_scriptmonitor_urlr   c                     | r|rt          d          | s|r|rt          d          |r|st          d          dS dS )a.  Shared create/update validation for job execution-mode invariants.

    ONE owner for the class: create_job and update_job both call this so an
    invariant enforced at create time cannot be violated through the update
    door (monitor jobs silently degrading when no_agent is flipped on, etc.).
    ua   monitor_script and monitor_url are mutually exclusive — a job can only have one monitor source.u   monitor_script/monitor_url cannot be combined with no_agent=True — the whole point of a monitor job is to suppress or wake the AGENT based on source changes. Use a plain no_agent script job instead.ud   no_agent=True requires a script — with no agent and no script there is nothing for the job to run.N)rQ   )r   r!  r  r   s       r   _validate_job_mode_invariantsr#    s      
+ 
0
 
 	
 	 
+ 
8 
P
 
 	

  
 
3
 
 	

 
 
 
r0   r   r   repeatdeliverorigincontext_fromenabled_toolsetsattach_to_sessionreasoning_effortc                 \   t          |          }||dk    rd}|d         dk    r|d}||rdnd}t          j                    j        dd         }t	                                                      }t          ||          }t          |          }t          |	          }t          |
d	
          }t          |t                    r!t          |          
                                nd}|pd}|rd |D             nd}|pd}t          |          }t          |          }t          |t                    r|nd}t          |          } t          |t                    r!t          |          
                                nd}!|!pd}!t          |t                    r!t          |          
                                nd}"|"pd}"t          |!|"||           t          |t                    r,|
                                r|
                                gnd}n&t          |t                    rd |D             pd}nd}t!          |           }#ddlm}$  |$|#|           |#p|r|d         ndp|r|ndpd}%t'          ||||          \  }&}'t)          |          }(|                    d          dk    rr|(p|                    d          p|})t,                              d|p|%dd         
                                |)t0                     t3          d|) dt0           d          i d|d|p|%dd         
                                d|#d|d|r|d         ndd|d|d|&d|'d|d |d!|d"|!d#|"d$dd%|d&|i d'|                    d(|          d)|dd*d+d	d,d-d.dd/dd0|d1|(d2dd3dd4dd5dd6dd7|d|d8|d9|}*|||*d:<   | | |*d;<   t5                      5  t7                      }+|+                    |*           t;          |+           ddd           n# 1 swxY w Y   |*S )<u  
    Create a new cron job.

    Args:
        prompt: The prompt to run (must be self-contained, or a task instruction when skill is set).
                Ignored when ``no_agent=True`` except as an optional name hint.
        schedule: Schedule string (see parse_schedule)
        name: Optional friendly name
        repeat: How many times to run (None = forever, 1 = once)
        deliver: Where to deliver output ("origin", "local", "telegram", etc.)
        origin: Source info where job was created (for "origin" delivery)
        skill: Optional legacy single skill name to load before running the prompt
        skills: Optional ordered list of skills to load before running the prompt
        model: Optional per-job model override
        provider: Optional per-job provider override
        base_url: Optional per-job base URL override
        script: Optional path to a script whose stdout feeds the job. With
                ``no_agent=True`` the script IS the job — its stdout is
                delivered verbatim. Without ``no_agent``, its stdout is
                injected into the agent's prompt as context (data-collection /
                change-detection pattern). Paths resolve under
                ~/.hermes/scripts/; ``.sh`` / ``.bash`` files run via bash,
                anything else via Python.
        context_from: Optional job ID (or list of job IDs) whose most recent output
                      is injected into the prompt as context before each run.
                      Useful for chaining cron jobs: job A finds data, job B processes it.
        enabled_toolsets: Optional list of toolset names to restrict the agent to.
                          When set, only tools from these toolsets are loaded, reducing
                          token overhead. When omitted, all default tools are loaded.
                          Ignored when ``no_agent=True``.
        workdir: Optional absolute path.  When set, the job runs as if launched
                from that directory: AGENTS.md / CLAUDE.md / .cursorrules from
                that directory are injected into the system prompt, and the
                terminal/file/code_exec tools use it as their working directory
                (via TERMINAL_CWD).  When unset, the old behaviour is preserved
                (no context files injected, tools use the scheduler's cwd).
                With ``no_agent=True``, ``workdir`` is still applied as the
                script's cwd so relative paths inside the script behave
                predictably.
        no_agent: When True, skip the agent entirely — run ``script`` on schedule
                and deliver its stdout directly. Empty stdout = silent (no
                delivery). Requires ``script`` to be set. Ideal for classic
                watchdogs and periodic alerts that don't need LLM reasoning.
        monitor_script: Optional path to a cheap monitor source script (same
                resolution/containment rules as ``script``: relative to
                ~/.hermes/scripts/, .sh/.bash via bash, else Python). Each
                tick the script runs FIRST and its output is hashed as exact
                bytes: unchanged output suppresses the agent run entirely
                (recorded as a silent 'no_change' tick); changed output
                injects a MONITOR CHANGE DETECTED block (unified diff + new
                output) into the prompt before a normal agent run. Scripts
                should emit stable output (no timestamps). Mutually exclusive
                with ``monitor_url``; incompatible with ``no_agent=True``.
        monitor_url: Optional http(s) URL used as the monitor source instead
                of a script — fetched with a bounded GET each tick. Same
                hash-suppression semantics as ``monitor_script``.
        reasoning_effort: Optional per-job reasoning effort pin. One of the
                canonical Hermes levels (none|minimal|low|medium|high|xhigh|
                max|ultra, case-insensitive). When set, it wins over BOTH the
                global ``agent.reasoning_effort`` and per-model
                ``agent.reasoning_overrides`` at fire time. Capability is NOT
                validated here: levels above what the resolved model supports
                are clamped or omitted by the provider transport at send time,
                exactly like config-set effort. Inert with ``no_agent=True``
                (no LLM call to configure). None/empty = unset (job follows
                config resolution, pre-existing behavior).

    Returns:
        The created job dict
    Nr   r  r  rg   r&  local   Tr   c                     g | ]D}t          |                                          #t          |                                          ES r/   r   rN   )r   ts     r   r  zcreate_job.<locals>.<listcomp>  s9    VVVas1vv||~~V3q66<<>>VVVr0   c                     g | ]D}t          |                                          #t          |                                          ES r/   r/  r  s     r   r  zcreate_job.<locals>.<listcomp>  s9    OOO1AOAOOOr0   )check_gateway_lifecycler   r  r  r  r  r   zKRejecting one-shot cron job '%s': run_at %s is outside the %ss grace windowr   Requested one-shot time  is more than &s in the past and cannot be scheduled.r   r   r   r   r   r  r  r  r  r  r   r  r   r!  monitor_stater'  r   r   r   r$  )timesr   r   r   r   r   paused_reason
created_atnext_run_atr;  rM  
last_errorlast_delivery_errorfailure_streakr%  r(  r  r)  r*  )r-  r   uuid4r   r$  r%  r   r  r   r   rN   r  r   r  r#  r   r   cron.lifecycle_guardr2  r  rv  r5   r`   ra   r>  rQ   r   r   r   r  ),r   r   r   r$  r%  r&  r   r   r  r  r  r   r'  r(  r  r  r)  r   r!  r*  parsed_schedulerY   r   normalized_skillsr  r  r  normalized_scriptnormalized_toolsetsnormalized_workdirnormalized_no_agentnormalized_attachnormalized_reasoning_effortnormalized_monitor_scriptnormalized_monitor_urlprompt_textr2  r   r  r  r;  r   r   r  s,                                               r   
create_jobrL    s   x %X..O fkk v&((V^ $1(('Z\\crc"F
--
!
!
#
#C-eV<<3E::6x@@6xVZ[[[/9&#/F/FPF))+++D)1TZjtVV3CVVVVpt-5+G44x..-78I4-P-PZ))VZ"=>N"O"O?I.Z]?^?^ hN 3 3 9 9 ; ; ;dh 9 AT9CKQT9U9U_S--33555[_3;t "!	   ,$$ 1=1C1C1E1EO**,,--4	L$	'	' OOOOOWSW"6**K =<<<<<K):;;;  Q<M$W$5a$8$8SW  Q  sF  ^P]n]n  LP  `  V`L(I$$$	) ) )%~ #?33K6""f,,1D $$X..:(Y-L"%++--!		
 	
 	
 Mv M M$M M M
 
 	

,f,1SbS)//11, 	+, 	#	,
 	):D"1%%, 	!, 	', 	., 	., 	', 	#, 	',  	3!," 	-#,( 	),* 	+,, 	O-, ,. 	O//	8DD/,0 	
 
1,8 	49,: 	;,< 	T=,> 	?,@ 	cA,B 	{C,D 	tE,F 	tG,H 	dI,J 	tK,L 	!M,P 	7Q,R 	&S,T 	/U,V 	%W, ,C` $#4  #."=	  {{C$              
 Js   "3P!!P%(P%c                 f    t                      }|D ]}|d         | k    rt          |          c S  dS )zGet a job by ID.r   N)r   r   rY   r  r   s      r   get_jobrO  
  sG    ;;D . .t9(----- 4r0   c                   H     e Zd ZdZdedeeeef                  f fdZ xZ	S )AmbiguousJobReferencez1Raised when a job name matches more than one job.refmatchesc           	          || _         || _        d                    d |D                       }t                                          d| dt          |           d| d           d S )N, c              3   &   K   | ]}|d          V  dS r   r/   )r   r  s     r   r   z1AmbiguousJobReference.__init__.<locals>.<genexpr>  s&      11A$111111r0   z
Job name 'u   ' is ambiguous — matches z jobs: z. Use the job ID instead.)rR  rS  r  super__init__r   )selfrR  rS  ids	__class__s       r   rX  zAmbiguousJobReference.__init__  s    ii1111111' ' 'W ' 'c ' ' '	
 	
 	
 	
 	
r0   )
r+   r,   r-   __doc__r   r   r
   r   rX  __classcell__)r[  s   @r   rQ  rQ    s^        ;;
C 
$tCH~*> 
 
 
 
 
 
 
 
 
 
r0   rQ  rR  c                 <   | sdS t                      }|D ]}|d         | k    rt          |          c S  |                                 fd|D             }|sdS t          |          dk    rt	          | d |D                       t          |d                   S )aU  Resolve a job reference (ID or name) to a job record.

    - Exact ID match wins (works even if a different job's name equals this ID).
    - Otherwise, case-insensitive name match.
    - If a name matches more than one job, raises AmbiguousJobReference so the
      caller can surface the matching IDs rather than silently picking one.
    Nr   c                 n    g | ]1}|                     d           pd                                k    /|2S )r   rK   )r5   r  )r   r  	ref_lowers     r   r  z#resolve_job_ref.<locals>.<listcomp>/  s>    RRR!f(;'B'B'D'D	'Q'QA'Q'Q'Qr0   rg   c                 ,    g | ]}t          |          S r/   r   r  s     r   r  z#resolve_job_ref.<locals>.<listcomp>4  s!    AAAq'**AAAr0   r   )r   r   r  r   rQ  )rR  r  r   name_matchesr`  s       @r   resolve_job_refrd     s      t;;D . .t9(----- 		IRRRRtRRRL t
<1#AALAAA
 
 	
 !a111r0   include_disabledc                    d t                      D             }| sd |D             }	 ddlm}  |d |D                       }n# t          $ r i }Y nw xY w|D ].}|                    |                    dd                    |d<   /|S )	z2List all jobs, optionally including disabled ones.c                 ,    g | ]}t          |          S r/   rb  r  s     r   r  zlist_jobs.<locals>.<listcomp>;  s!    :::!!$$:::r0   c                 >    g | ]}|                     d d          |S )r   Tr   r  s     r   r  zlist_jobs.<locals>.<listcomp>=  s+    :::a155D#9#9::::r0   r   )latest_executionsc                 :    g | ]}|                     d d          S )r   rK   r   )r   r   s     r   r  zlist_jobs.<locals>.<listcomp>A  s&    #F#F#F#CGGD"$5$5#F#F#Fr0   r   rK   latest_execution)r   cron.executionsri  r_   r5   )re  r  ri  latestr   s        r   	list_jobsrn  9  s    ::ikk:::D ;::4:::555555""#F#F#F#F#FGG    @ @"(**SWWT2->->"?"?Ks   A AAupdatesc                 \
   t                               |pi           }|r2t          dd                    t	          |                               t                      5  t                      }t          |          D ]\  }}|d         | k    rd|v r$|d         }|dv rd|d<   nt          |          |d<   dD ]M}||v rG||         }t          |t                    r!t          |                                          nd}|pd||<   Nd|v rt          |d                   |d<   t          |          }	t          i ||          }
h d	                    |          r|
                    d
          }t          |t                    r!t          |                                          nd}t!          |
                    d          pd|
                    d          pdt#          |
                    d                    |pd           d|v }t#          h d                    |                    ot          |
          |	k    }d|v sd|v rJt%          |
                    d          |
                    d                    }||
d<   |r|d         nd|
d<   |r!|
d         }t          |t                    rt'          |          }||
d<   |                    d|                    d|
                    d                              |
d<   |
                    d          dk    rt)          |          }||                    d          dk    rh|                    d          p|}t*                              d|
                    d|           |t.                     t          d| dt.           d          ||
d<   |rlt1          |
                    d           |
                    d!          |
                    d"          |
                    d          #          \  }}||
d$<   ||
d%<   |
                    d&d'          r|
                    d          dk    r|
                    d          srt)          |
d                   }|V|
d                             d          dk    r7|
d                             dd(          }t          d| d)t.           d*          ||
d<   |
||<   t3          |           t5          ||                   c cddd           S 	 ddd           n# 1 swxY w Y   dS )+zCUpdate a job by ID, refreshing derived schedule fields when needed.z%Cron job field(s) cannot be updated: rU  r   r  >   FNrK   N)r   r!  r*  >   r   r  r!  r   r   r   r!  r  r   >   r  r  r  r  r   r   r   r   r   r   r   r  r  r   zRRejecting one-shot cron job update '%s': run_at %s is outside the %ss grace windowr   r4  r5  r6  r;  r  r  r  r3  r  r  r   Tr    is in the past (grace window: zs) and cannot be scheduled.)_IMMUTABLE_JOB_FIELDSintersectionrQ   r  sortedr   r   	enumerater  r   r   rN   r  r  r   r5   r#  r   r   r-  rv  r`   ra   r>  r  r  r   )rY   ro  
bad_fieldsr  r  r   _wd
_mon_field_mvprevious_inference_axesupdated_upd_scriptschedule_changedinference_fields_changedrB  updated_scheduleupdated_next_runr   r  r  rt  s                        r   
update_jobr  I  sp   
 '33GMrBBJ 
SDIIfZ>P>P4Q4QSS
 
 	
 
 x2 x2{{oo v	2 v	2FAs4yF"" G##i(+++)-GI&&);C)@)@GI& @ 6 6
((!*-C.8c.B.BL#c((..***C*-+GJ' "W,,.I.// /*+ 'A&E&E#)*<S*<G*<==G GFFSST[\\ %kk(33:D[RU:V:V`c+..44666\`-KK 0119TKK..6$Z0011'4	    *W4'+===JJ7SS( ( (Q,W559PP % 7""g&8&8$9'++g:N:NPWP[P[\dPePe$f$f!$5!;L#V#4Q#7#7RV  #>#*:#6  .44 ;'56F'G'G$*:GJ'.5kk&$((GKK@R4S4STT/ /*+ ;;w''833'78H'I'I$ )0,0088FBB!1!5!5h!?!?!SCS>#KK77"1   )]v ] ]4] ] ]   .>GM*' ;4U$[[44!++g..$[[44$[[44	5 5 51!> 0A+,,:(){{9d++ 2G0D0D0P0PY`YdYderYsYs0P+GJ,?@@#
(;(?(?(G(G6(Q(Q$Z044XyIIF$]6 ] ]*?] ] ]   *2&DGdOOO(a1111qx2 x2 x2 x2 x2 x2 x2 x2v	2x2 x2 x2 x2 x2 x2 x2 x2 x2 x2 x2 x2 x2 x2 x2r 4s   R(T!!T%(T%reasonc                     t          |           }|sdS t          |d         ddt                                                      |d          S )z:Pause a job without deleting it. Accepts a job ID or name.Nr   Fr   )r   r   r   r9  rd  r  r$  r%  )rY   r  r   s      r   	pause_jobr    s[    
&
!
!C tD	$0022#		
 	
  r0   c           	      :   t          |           }|sdS t          |d                   }|V|d                             d          dk    r7|d                             dd          }t          d| dt           d	          t          |d
         dddd|d          S )zWResume a paused job and compute the next future run from now. Accepts a job ID or name.Nr   r  r  r   r   zCannot resume: one-shot time rq  zs) and will never fire.r   Tr   r   r   r   r9  r;  )rd  rv  r5   rQ   r>  r  )rY   r   r;  r   s       r   
resume_jobr    s    
&
!
!C t"3z?33Ks:226::fDDZ$$Xy99MF M M3M M M
 
 	
 D	 !&	
 	
	 	 	r0   c           	          t          |           }|sdS t          |d         ddddt                                                      d          S )zKSchedule a job to run on the next scheduler tick. Accepts a job ID or name.Nr   Tr   r  r  )rY   r   s     r   trigger_jobr    s^    
&
!
!C tD	 !&==2244	
 	
	 	 	r0   c                    t          |           }|sdS |d         t                      5  t                      }t          |          }fd|D             }t          |          |k     rt	                    }t          |h           |                                rt          j        |           	 ddl	m
}  |           n-# t          $ r  t                              dd	           Y nw xY wt                      j                                         d
 }t"          5  t$                              |d           ddd           n# 1 swxY w Y   	 ddd           dS 	 ddd           n# 1 swxY w Y   dS )zRemove a job by ID or name.Fr   c                 ,    g | ]}|d          k    |S r  r/   )r   r  canonical_ids     r   r  zremove_job.<locals>.<listcomp>	  s'    ;;;a1T7l#:#:#:#:#:r0   r  r   )clear_notepadz*Failed to clear notepad for removed job %sTr\   r   N)rd  r   r   r   r   r  r   shutilrmtreecron.notepadr  r_   r`   rj  r?   r(   r:   r   r"   pop)rY   r   r  original_lenjob_output_dirr  
_fence_keyr  s          @r   
remove_jobr  
	  sO   
&
!
!C ut9L	  {{4yy;;;;4;;;t99|## -\::Nd7777$$&& .n---666666l++++   @ 4       011:BBDDVVVVJ( 8 8!%%j$7778 8 8 8 8 8 8 8 8 8 8 8 8 8 8;        $	              < 5sZ   BE24CE2'C0-E2/C004E2$E E2E	E2E	E22E69E6)expected_fire_ownerr   delivery_errorstatusr  c          	          t          |           5 }|s	 d d d            dS t          | |||||          cd d d            S # 1 swxY w Y   d S )NFr  r  )r   _mark_job_run_locked)rY   r  r   r  r  r  r   s          r   mark_job_runr  1	  s     
		 

8 	

 

 

 

 

 

 

 

 $ 3
 
 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

s   AAA	Afieldc                    t                      5  t                      }t          |          D ]{\  }}|d         | k    rjt          |                    |                    }|rd||<   n|                    |d           ||k    r|||<   t          |           |c cddd           S |	 ddd           n# 1 swxY w Y   dS )a  Set/clear a persisted alert-dedup marker; return the PRIOR value.

    The marker records that the operator was already alerted about this
    job's condition, so the scheduler alerts exactly once and stays silent
    on subsequent ticks until the condition heals (same alert-once shape as
    the dead-pin auto-pause in #73506). Persisted on the job record so the
    dedup survives gateway restarts. Fields: ``preflight_alerted`` (blocked
    config, T1-26) and ``drift_alerted`` (#44585 drift-guard skip).
    r   TNF)r   r   ru  r   r5   r  r  )rY   r  r   r  r  r   priors          r   _set_alert_flagr  G	  s/    
  {{oo 
	 
	FAs4yF""SWWU^^,, )!%CJJGGE4(((E>>!DGdOOO        #
	               5s   BB6(B66B:=B:c                 $    t          | d|          S )zCSet/clear the preflight alert-dedup marker; return the PRIOR value.preflight_alertedr  )rY   r   s     r   _set_preflight_alertedr  a	  s    6#6>>>r0   c                 "    t          | d          S )zAMark the job as preflight-alerted; return True if it already was.Tr  rY   s    r   mark_preflight_alertedr  f	  s    !&$///r0   c                 &    t          | d           dS )z@Clear the preflight alert-dedup marker (config validates again).FNr  r  s    r   clear_preflight_alertedr  k	  s    65)))))r0   c                 $    t          | dd          S )z=Mark the job as drift-alerted; return True if it already was.drift_alertedTr  r  s    r   mark_drift_alertedr  p	  s    6?D999r0   c                 (    t          | dd           dS )z>Clear the drift alert-dedup marker (resolution matches again).r  FNr  r  s    r   clear_drift_alertedr  u	  s    FOU33333r0   detailc                 p   t                      5  t                      }t          |          D ]p\  }}|d         | k    r_t                                                      t          |pd          dd         d|d<   |||<   t          |            ddd           dS q	 ddd           n# 1 swxY w Y   dS )	un  Durably record that a scheduled fire could not be handed to the runner.

    Written by the dashboard fire webhook when the loopback forward to the
    gateway api_server fails (gateway unreachable / listener not bound) —
    the shape behind "job runs manually but never auto-fires". Without this
    stamp the miss is invisible outside gui.log: no execution row is created
    (the claim never happens) and ``last_status``/``last_error`` only cover
    runs that actually started.

    Stored as ``last_fire_error`` (``{"at": iso, "detail": str}``) on the job
    record so `cronjob list`, the CLI, and the dashboard all surface it.
    Cleared by the next successful run (``mark_job_run``). Repeated failures
    overwrite in place — latest miss wins; per-fire history lives in the
    scheduler's own logs.

    Returns True when a job record was found and stamped.
    r   rK   Ni  )atr  last_fire_errorTF)r   r   ru  r$  r%  r   r  )rY   r  r  r  r   s        r   note_fire_forward_failurer  z	  s#   $ 
 
 
{{oo 	 	FAs4yF""%--1133!&,B//5* *%& Q$
 
 
 
 
 
 
 
 #	
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 5s   BB+B++B/2B/r  c                   t                      5  t                      }t          |          D ]\  }}|d         | k    r|l|                    d          }	t	          |	t
                    r|	                    d          |k    r)t                              d|             ddd           dS t                      	                                }
|
|d<   |p|rdnd	|d
<   |s|nd|d<   |rB|
                    dd           |
                    dd           |
                    dd           |rd|d<   n*t          |                    d          pd          dz   |d<   ||d<   d|d<   |                    d          d|d<   |                    d          r|d         }|                    d          }|                    dd          }|                    di                               d          }|dk    o|duo|dk    o|dk    }|s
|dz  }||d<   |8|dk    r2||k    r,d|d<   d|d<   d|d<   t          |            ddd           dS t          |d         |
          |d<   |d         |                    di                               d          }|dv rdd	|d<   |                    d          sd|d<   t                              d |                    d!|                    dd"                    |           n)d|d<   d|d<   n|                    d          d#k    rd$|d<   t          |            ddd           dS t                              d%|            	 ddd           dS # 1 swxY w Y   dS )&u  
    Mark a job as having been run.
    
    Updates last_run_at, last_status, increments completed count,
    computes next_run_at, and auto-deletes if repeat limit reached.

    ``delivery_error`` is tracked separately from the agent error — a job
    can succeed (agent produced output) but fail delivery (platform down).

    ``status`` overrides the derived ``last_status`` ("ok"/"error") with a
    specific terminal status for this run — e.g. ``"blocked_config"`` when
    the pre-dispatch configuration validation refused to run the agent
    (T1-26), so `cronjob list` distinguishes "your config is broken" from
    "the run itself failed".
    r   Nr   r   zMmark_job_run: job_id %s fire claim owner changed; discarding stale completionFr;  okr   rM  r<  r  r  r  r   r>  rg   r=  	run_claimr$  r8  r   r   r  r  r   r   r;  T>   r   r  ztFailed to compute next run for recurring schedule (is the 'croniter' package installed in the gateway's Python env?)zyJob '%s' (%s) could not compute next_run_at; leaving enabled and marking state=error so the job is not silently disabled.r   r   r   r   z0mark_job_run: job_id %s not found, skipping save)r   r   ru  r5   r   r   r`   ra   r$  r%  r  r  r  rv  r   )rY   r  r   r  r  r  r  r  r   r   r   r$  r8  r   r  preclaimed_oneshots                   r   r  r  	  s   0 
 { {{{oo v	 v	FAs4yF""&2GGL11E%eT22 %eiiooI\6\6\:"  
  %{ { { { { { { { "mm--//%(M"%+%M0LWM"18$BEEdL!  5GG/666GGOT222 GG-t444  T,-C()),/8H0I0I0NQ,O,ORS,SC()-;)* %)L! 77;''3'+C$ 778$$ $ ]F"JJw//E &

; : :I77:r2266v>>D *!-*!AI* &M	 ' . 8!Q	.7{+ (UQYY9;M;M */I'2G-1M*!$#k{ { { { { { { {p &6c*os%K%KM" }%-77:r2266v>>D333'.G"ww|44 !J  -
 <  GGFCGGD#,>,>??     */I'2GWWW%%11#.CL$q{ { { { { { { { #n 	I6RRRw{ { { { { { { { { { { { { { { { { {s&   BM1.FM1C+M1M11M58M5c                 F   |                      d          dS 	 |                      d          pi }|                      d          pi }d|                      d           d|                      d           d	|                     d
d           d|                     dd           d|                     dd           d|                     dd           dt                                                       d}t          |                      dd          |           t                              d|                      d|                      dd                               dS # t          $ r:}t                              d|                      d          |           Y d}~dS d}~ww xY w)u  Leave an operator-visible trace when a wedged one-shot is removed.

    A finite one-shot whose dispatch was claimed (``repeat.completed`` >=
    ``repeat.times``) but which never reached ``mark_job_run`` (``last_run_at``
    is null) was interrupted mid-run — scheduler restart, gateway kill, or a
    non-Exception escape (#73973). The recovery guards remove such jobs so
    they stop appearing due, but a silent removal leaves the user with no
    output, no error, and no job record. Write a small diagnostic file into
    the job's output directory so the removal is observable and debuggable.

    Best-effort: diagnostics must never break the removal itself.
    r;  Nr$  r  z7# Cron job removed without producing output

- job id: r   z	
- name: r   z
- dispatch claimed: r   r   r   r8  z
- run claimed at: r  r   z by r   z
- removed at: u   

This one-shot job's dispatch was claimed, but the run never completed (`last_run_at` was never written) — the scheduler process was most likely killed or restarted mid-execution. The job has been removed to stop it re-firing; recreate it to run again.
rK   uX   Job '%s': removed without a completed run — diagnostic written to its output directoryz8Failed to write wedged-oneshot diagnostic for job %r: %s)r5   r$  r%  save_job_outputr`   ra   r_   rj  )r   r$  r   r   r   s        r    _write_wedged_oneshot_diagnosticr  0
  s    ww})
""(b$$*

 
wwv
 
 $*::k3#?#?
 
 CI**WVYBZBZ
 
 "'4!;!;	
 
 BG4QZA[A[	
 

 )]]4466
 
 
 	 	b))4000#GGFCGGD#..//	
 	
 	
 	
 	

  
 
 
FGGDMM1	
 	
 	
 	
 	
 	
 	
 	
 	

s   EE 
F &/FF c                 :   t                      5  t                      }t          |          D ]8\  }}|d         | k    r|                    di                               d          dk    r ddd           dS |                    d          }|s ddd           dS |                    d          }||d	k    r ddd           dS |                    d
d	          }||k    r|                    d          qd|d<   d
|d<   d|d<   t	          |           t
                              d|                    d|                    dd                    ||            ddd           dS |                    |           t	          || h           t          |           t
                              d|                    d|                    dd                    ||            ddd           dS |dz   |d
<   t	          |           t
          	                    d|                    d|                    dd                    |d
         |            ddd           dS t
          	                    d|            	 ddd           dS # 1 swxY w Y   dS )up  Atomically claim a finite one-shot job dispatch BEFORE execution.

    Increments ``repeat.completed`` under the cross-process jobs lock and
    persists the claim immediately, so that if the tick dies mid-execution
    (gateway kill, OOM, segfault, hard-timeout) the dispatch is not lost.
    This converts finite one-shot jobs from *at-least-once* to *at-most-times*
    semantics — a job that self-destructs fires at most ``repeat.times`` times
    instead of infinitely (issue #38758).

    Returns ``True`` if the caller may proceed to run the job, ``False`` if the
    dispatch limit is already reached (in which case the stale job is removed).

    Only claims jobs with ``schedule.kind == "once"`` and ``repeat.times > 0``.
    Recurring jobs (they use ``advance_next_run``) and infinite-repeat / no-repeat
    jobs are left unchanged and always allowed to proceed.
    r   r   r  r  NTr$  r8  r   r   r;  Fr   r   r;  u>   Job '%s': dispatch limit reached (%d/%d) — marking completedr   r   r  u5   Job '%s': dispatch limit reached (%d/%d) — removingrg   z Job '%s': claimed dispatch %d/%du|   claim_dispatch: job_id %s not in store — proceeding without claim (handed-in job dict; nothing to persist a claim against))
r   r   ru  r5   r  r`   infor  r  rj  )rY   r  r  r   r$  r8  r   s          r   claim_dispatchr  \
  s   " 
 > >{{oo 5	 5	FAs4yF""wwz2&&**622f<<> > > > > > > > WWX&&F > > > > > > > > JJw''E}

> > > > > > > > 

;22IE!!77=))5
 &+C	N#.CL)-C&dOOOKKXc(:(:;;!	   !?> > > > > > > >H $VH55550555KGGFCGGD#$6$677	   [> > > > > > > >^ #,a-F;dOOOLL2c 2 233{#	   o> > > > > > > >r 	G	
 	
 	

 }> > > > > > > > > > > > > > > > > >s9   AJ;J JBJ.A<J7A#J'JJJc                D   t                      5  t                      }|D ]}|                    d          | k    r|                    di                               d          dk    r ddd           dS |                    d          }t          |t                    r|                    d          |k    r ddd           dS t                                                      |d	<   t          |            ddd           d
S 	 ddd           n# 1 swxY w Y   dS )a4  Refresh a one-shot's ``run_claim`` timestamp while its run is alive.

    Called periodically from the scheduler's run monitor (#62002) so a
    legitimately long run keeps its claim fresh: an expired claim then really
    does mean "the claiming process died", and neither another process's tick
    nor this process's own next tick will re-dispatch or stale-remove the job
    while the run is in flight. mark_job_run() clears the claim on completion.

    ``expected_owner`` is the stable owner copied from the dispatched job. The
    compare-and-refresh prevents a stale runner that resumes after a long sleep
    from extending a claim another scheduler process has since taken over.

    Returns True if this owner's one-shot claim was refreshed; False when the
    job, claim, or ownership no longer matches.
    r   r   r  r  NFr  r   r  Tr   r   r5   r   r   r$  r%  r  rY   r   r  r   r   s        r   heartbeat_run_claimr  
  s     
  {{ 
	 
	Cwwt}}&&wwz2&&**622f<<        GGK((EeT** eiioo.O.O        &--1133E$KdOOO       
	               5s   AD7AD3DDDc                    t                      5  t                      }|D ]}|                    d          | k    r|                    di                               d          dk    r ddd           dS |                    d          "d|d<   t          |            ddd           dS  ddd           dS 	 ddd           n# 1 swxY w Y   dS )	ap  Clear a one-shot job's ``run_claim`` when its dispatch fails.

    ``get_due_jobs`` stamps a ``run_claim`` before returning a one-shot as
    due (#59229).  ``mark_job_run`` clears it on *successful* completion.
    When dispatch itself fails (interpreter shutdown, executor submit error,
    execution-creation error) the job never reaches ``mark_job_run`` and the
    stale claim blocks re-dispatch until the TTL expires (default 30 min).

    Calling this on every early-exit path restores the "the job stays due
    and will fire on the next healthy tick" invariant that the scheduler
    comment promises (#86522).
    r   r   r  r  NFr  T)r   r   r5   r  rN  s      r   clear_run_claimr  
  sr    
  {{ 		 		Cwwt}}&&wwz2&&**622f<<        ww{##/#'K $               		               5s   AC	7*C	.C		CCc                    t          |           }|sdS t                      5  t                      }t                                                      }d}|D ]v}|d         |vr|                    di                               d          }|dvr;t          |d         |          }|r#||                    d          k    r
||d<   |dz  }w|rt          |           |cddd           S # 1 swxY w Y   dS )	u  Batch form of :func:`advance_next_run` for the due-dispatch loop.

    One ``load_jobs()`` + at most one ``save_jobs()`` for the whole due
    set, instead of one of each per job — the per-job form costs
    O(N loads + N saves) for N due jobs (~110 ms at N=50, measured), the
    batch form O(1 + 1) (~2 ms). ``job_ids`` may contain ids of one-shot
    or unknown jobs; they are skipped exactly as the per-job form skips
    them. Returns the number of jobs whose ``next_run_at`` was advanced.

    Crash semantics: the batch persists once at the end, so a crash
    mid-batch re-fires the whole set on restart (at-least-once burst)
    rather than advancing a prefix — acceptable given the sub-10ms window,
    and identical to the per-job form once the batch completes.
    r   r   r   r  >   r   r  r;  rg   N)rB   r   r   r$  r%  r5   rv  r  )job_idsrZ  r  r   advancedr   r  new_nexts           r   advance_next_runsr  
  sY    g,,C q	  {{mm%%'' 		 		C4y##77:r**..v66D///'J==H H(>(>>>%-M"A 	dOOO!                 s   B<C++C/2C/c                 *    t          | g          dk    S )u  Preemptively advance next_run_at for a recurring job before execution.

    Call this BEFORE run_job() so that if the process crashes mid-execution,
    the job won't re-fire on the next gateway restart.  This converts the
    scheduler from at-least-once to at-most-once for recurring jobs — missing
    one run is far better than firing dozens of times in a crash loop.

    One-shot jobs are left unchanged so they can still retry on restart.

    Returns True if next_run_at was advanced, False otherwise.
    rg   )r  r  s    r   advance_next_runr    s     fX&&!++r0   c                      t          j        dd                                          } | r| S 	 ddl}|                                }n# t
          $ r d}Y nw xY w| dt          j                     S )zStable-ish identifier for claim attribution/debugging (NOT correctness).

    Uses ``HERMES_MACHINE_ID`` if set, else hostname + pid. The CAS correctness
    comes from the file lock + the fresh-claim check, not from this value.
    HERMES_MACHINE_IDrK   r   Nr   :)rL   rM   rN   socketgethostnamer_   getpid)explicitr  hosts      r   _machine_idr     s     y,b117799H !!##   ""RY[["""s   A AAi,  claim_ttl_secondsforce
return_jobr  r  r  c                    t          |           5 }|s	 d d d            dS t          | |||          cd d d            S # 1 swxY w Y   d S )NFr  )r   _claim_job_for_fire_locked)rY   r  r  r  r   s        r   claim_job_for_firer  1  s     
		 
8 	
 
 
 
 
 
 
 
 */!	
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   A A  AAc                   t                      5  t                      }|D ]}|d         | k    r|st          |          s ddd           dS t                      }|                    d          }|rn	 t          t          j        |d                             }||z
                                  }	d|	cxk    r|k     rn n ddd           dS n# t          $ r Y nw xY w|rd|d<   d	|d
<   d|d<   d|d<   t                       dt          j                    j         }
|                                |
d|d<   |                    di                               d          }|dv r/t          |d         |                                          }|r||d<   t!          |           |rt#          j        |          ndc cddd           S 	 ddd           dS # 1 swxY w Y   dS )u2  Atomically claim a job for a single external 'fire' (multi-machine
    at-most-once). Returns True iff THIS caller won the claim.

    Used by the external-provider fire path (``CronScheduler.fire_due``) when an
    external scheduler (Chronos) signals a job is due across N gateway replicas:
    exactly one wins. Single-machine deployments always win.

    Under the file lock: reject if the job is missing/disabled/paused. An
    explicit manual fire may pass ``force=True`` to atomically enable and
    resume the job as part of the claim; external scheduler callbacks must
    leave it false so a stale callback cannot resurrect a paused job. If a
    fresh claim (younger than ``claim_ttl_seconds``) already exists, lose.
    Otherwise stamp a ``fire_claim`` and, for recurring jobs, advance
    ``next_run_at`` (mirrors ``advance_next_run``'s at-most-once bump so a stale
    re-delivery for the old time can't re-fire). One-shots keep ``next_run_at``
    but the fresh ``fire_claim`` blocks a duplicate retry for the same fire.
    ``mark_job_run`` clears the claim on completion so a re-armed recurring job
    is claimable again next fire.

    The stale-claim TTL means a machine that crashed after claiming but before
    completing doesn't wedge the job forever — after the TTL another fire can
    reclaim it.
    r   NFr   r  r   Tr   r   r   r   r9  r  r  r   r   r  >   r   r  r;  )r   r   r   r$  r5   r2  r   r"  rN  r_   r  r   r?  r   r%  rv  r  copydeepcopy)rY   r  r  r  r  r   r   existing
claimed_at_ageownerr  nxts                r   r  r  C  s   < 
 , ,{{ )	> )	>C4yF""
  !5!5 , , , , , , , , --Cww|,,H !.x/Ehtn/U/U!V!VJ  *,;;==DD4444#444444$1, , , , , , , ,2 !   D ,!%I*G#'K '+O$ #}}99tz||'799E'*}}e D DC77:r**..v66D+++&s:HH -),C&dOOO)3=4=%%%==W, , , , , , , ,X Y, , , , , , , , , , , , , , , , , ,sI   3G%G5ACGG
CGCC GGG!G   c                     	 ddl m}   |             pi }t          |t                    r|                    di           ni }t          |                    dt                              S # t          $ r t          t                    cY S w xY w)a  Resolve the completed one-shot retention window from config.

    ``cron.completed_retention_days`` (number, default
    ``COMPLETED_ONESHOT_RETENTION_DAYS``). A non-positive value disables the
    sweep, retaining completed one-shot records indefinitely.
    r   load_configr   completed_retention_days)r  r  r   r   r5   rP    COMPLETED_ONESHOT_RETENTION_DAYSr_   r  r  r  s      r   !_completed_oneshot_retention_daysr    s    
7111111kmm!r*4S$*?*?G37762&&&RLL*,L 
 
 	

  7 7 75666667s   A&A) )B
	B
raw_jobsc                   t                      }|dk    rdS |t          |          z
  }d}t          |           D ]}	 |                    d          dk    r|                    d          }t	          |t
                    r|                    d          nd}|d	k    rf|                    d
          }	t	          |	t                    s	 t          t          j	        |	                    }
n# t          $ r Y w xY w|
|k    r|                     |           d}|                    d          }|$|r"|                    t          |                     t                              d|                    d|                    dd                    |	|           d# t          $ r5 t                              d|                    dd          d           Y w xY w|S )u{  Prune terminal ``state == "completed"`` one-shot records past retention.

    Mutates *raw_jobs* in place; returns True when anything was removed (the
    caller persists). Ids removed are added to *removed_ids* when provided so
    ``save_jobs``'s shrink-merge guard (#80624) allows the intentional delete.
    Only one-shot (``schedule.kind == "once"``) records in the terminal
    completed state are candidates; recurring jobs and non-terminal one-shots
    are never touched. Age is measured from ``last_run_at`` — a completed
    record without a parseable ``last_run_at`` is kept (never guess a record
    into deletion).
    r   F)daysr   r   r   r  Nr  r;  Tr   zNJob '%s': pruning completed one-shot record (finished %s, retention %.1f days)r   r   z/Retention sweep skipped malformed job record %rr\   )r  r   r   r5   r   r   r   r2  r   r"  r_   remover  r`   r  rj  )r  r   r  retention_dayscutoffremovedrjr   r  rO  rP  rids               r   _sweep_completed_oneshotsr    s   " 788Nu9.1111FG8nn " "!	vvg+--vvj))H+5h+E+EO8<<'''4Dv~~vvm,,Hh,, +H,B8,L,LMM   f$$OOBG&&,,C&3&C)))KK5vrvvdC0011     	 	 	LLAtS!!      	 NsI   FAF!*F!C/.F/
C<9F;C<<	FBF;GGc                    t          |           5 }|s	 d d d            dS t          | |          cd d d            S # 1 swxY w Y   d S )NF)r   )r   _heartbeat_fire_claim_locked)rY   r   r   s      r   heartbeat_fire_claimr    s    			 
8 	
 
 
 
 
 
 
 
 ,)
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   >>AAc                   t                      5  t                      }|D ]}|                    d          | k    r|                    d          }t          |t                    r|                    d          |k    r ddd           dS t                                                      |d<   t          |            ddd           dS 	 ddd           n# 1 swxY w Y   dS )a  Refresh an active ``fire_claim`` without extending another owner's lease.

    A cron execution can legitimately outlive the fire-claim TTL.  The shared
    run wrapper calls this periodically so another scheduler process cannot
    treat a live execution as abandoned and dispatch it again.  Comparing the
    owner copied at dispatch prevents a stale runner from refreshing a claim
    that has since been recovered by another process.
    r   r   r   NFr  Tr  r  s        r   r  r    sR    
 
 
{{ 	 	Cwwt}}&&GGL))EeT** eiioo.O.O
 
 
 
 
 
 
 
 &--1133E$KdOOO
 
 
 
 
 
 
 
	
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 5s   A1C3CC!Cc                  n    t                      5  t                      cddd           S # 1 swxY w Y   dS )u  Get all jobs that are due to run now.

    For recurring jobs (cron/interval), if the scheduled time is stale (more
    than one period in the past, e.g. because the gateway was down OR because a
    long-running previous execution overran the interval), the accumulated
    missed runs are collapsed — ``next_run_at`` is fast-forwarded to the next
    future occurrence so a backlog does NOT burst-fire on restart — but the job
    still fires ONCE now. This prevents the perpetual-defer loop (#33315) where
    a job whose runtime exceeds ``interval + grace`` would be skipped forever.

    Note: firing once on catch-up flows through ``mark_job_run``, so a job with
    a ``repeat.times`` limit consumes one of its runs on that catch-up fire.
    N)r   _get_due_jobs_lockedr/   r0   r   get_due_jobsr    s{     
 & &#%%& & & & & & & & & & & & & & & & & &s   *..c                     t                      } t                      d}t                      }D ]R}|                    d          s;|                    dd          pt          j                    j        dd         |d<   d}Sd t          j	                  D             }g }|D ]1}t          |                    d          t                    si |d<   d}2D ]1}t          |                    d          t                    si |d<   d}2|D ]}|                    d	          }|lt          |t                    s|                    d	d           d}G	 t          j        |           ]# t          $ r |                    d	d           d}Y w xY wD ]}|                    d	          }|lt          |t                    s|                    d	d           d}G	 t          j        |           ]# t          $ r |                    d	d           d}Y w xY w|D ]}|                    d
          }|.t          |t                    s|                    d
d           d}Gt          |t                    r>	 t          j        |           r# t          $ r |                    d
d           d}Y w xY wD ]}|                    d
          }|.t          |t                    s|                    d
d           d}Gt          |t                    r>	 t          j        |           r# t          $ r |                    d
d           d}Y w xY wt!                      }	t#          | |          rd}fd|D             }|D ]}
	 |
                    dd          st%          |
          r|
                    d          }t&                              d|
                    d|          |           D ]n}|                    d          |k    rd|d<   d|d<   |                    d          s|                                 |d<   |                    d          sd|d<   d} |
                    d          }|r|
                    di                               d          dk    ro	 t-          t          j        |d                             }| |z
                                  }d|cxk    r|	k     rn nvn# t0          t2          t4          f$ r Y nw xY w|
                    d	          }|s|
                    di           }|                    d          }t7          || |
                    d
                    }|rdnd}|s*|dv r&t9          ||                                           }|r|}|s1||
d	<   |}t&                              d|
                    d|
                    dd                    ||           D ]}|d         |
d         k    r	||d	<   d} nt          j        |          }|
                    di           }|                    d          }t-          |          }|dk    r|| k    rt=          ||           rt?          ||           rt9          ||                                           }|rt&                              d |
                    d|
                    dd                    |                                 |                                  |           D ]}|d         |
d         k    r	||d	<   d} n|d!v r|| k    rtC          |
||           r |
                    d          }|d"k    r|                                 }| }nbt9          ||                                           }	 |r!t-          t          j        |                    nd}n# t2          t4          f$ r d}Y nw xY w|rj|h||k     rbt&          "                    d#|
                    d|          ||           tG          |
|           ||
d	<   |}D ]}|d         |k    r	||d	<   d} n|| k    rtI          |          }|dv r| |z
                                  |k    rt9          ||                                           }|rtt&                              d$|
                    d|
                    dd                    |||           D ]}|d         |
d         k    r	||d	<   d} ntK                       |dk    rl|
                    d%          }|rT|                    d&          }|                    d'd          }|&|dk    r||k    rtM          |
                    dd(                    rGt&                              d)|
                    d|
                    dd                    ||           wt&                              d*|
                    d|
                    dd                    ||           D ]U}|d         |
d         k    rA'                    |           |(                    t          |d                              d} nVtS          |
           %|dk    rH|                                 tU                      d+}||
d<   D ]}|d         |
d         k    r	||d<   d} n|+                    |
           # t          $ rI t&          ,                    d,|
                    d          p|
                    d          pd           Y w xY w|rt[          |pd           |S )-zLInner implementation of get_due_jobs(); must be called with _jobs_lock held.Fr   rY   Nr-  Tc                 ,    g | ]}t          |          S r/   )r   r  s     r   r  z(_get_due_jobs_locked.<locals>.<listcomp>/  s!    DDDq""DDDr0   r   r;  r;  r  c                 L    g | ]t          fd D                        S )c              3   p   K   | ]0}|                     d                                d           k    V  1dS r   r   )r   r  r  s     r   r   z2_get_due_jobs_locked.<locals>.<listcomp>.<genexpr>  s<      &V&Vrrvvd||quuT{{'B&V&V&V&V&V&Vr0   )r  )r   r  r  s    @r   r  z(_get_due_jobs_locked.<locals>.<listcomp>  s<    WWWa3&V&V&V&VX&V&V&V#V#VWWWWr0   r   zsJob '%s' (%s) has pause markers while enabled=true; self-disabling so it cannot fire (pause must be authoritative).r   r   r   r   r9  z+auto-disabled: enabled+paused contradictionr  r  r  r  r   r:  zone-shot>   r   r  z4Job '%s' had no next_run_at; recovering %s run at %sr   r   zlJob '%s' next_run_at offset changed (%s -> %s). Recomputing cron run to preserve local wall-clock intent: %s)r   r  r  u   cron.persisted_error.recovered job='%s' id=%s — recurring job wedged in stale last_status=error without re-firing for a full cadence; re-arming next_run_at to %s so it re-dispatches without force-run/resumez~Job '%s' missed its scheduled time (%s, grace=%ds). Running now; next run provisionally set to: %s (re-anchored on completion)r$  r8  r   rK   ui   Job '%s': dispatch limit reached (%d/%d) but its run is still in flight in this process — keeping entryuN   Job '%s': one-shot dispatch limit reached (%d/%d) — removing stale due entryr  z.Skipping malformed cron job %r during due scan).r$  r   rB   r5   r  r   r?  r   r  r  r   r   r   r   r"  r_   rX   r  r   r`   r   r%  r2  rN  KeyErrorrQ   rR   r@  rv  r  r7  r9  r5  rS  ra   rm  rH  r  rb   r  r  r  r  r   	exceptionr  )r   
needs_saveintentionally_removedr  r  duer  nrlr_run_claim_ttlr   jidexisting_claimr  r  rt  r   r  recovered_nextrecovery_kindraw_next_run_dtnext_run_dtr  recovered_next_dtrG  r$  r8  r   r   r  s                                @r   r  r    sl   
--C{{HJ&)ee   vvd|| 	vvh--F1A#2#1FBtHJDDDM(,C,CDDDD
C   !%%
++T22 	AjMJ  "&&,,d33 	BzNJ  & &UU=!!>b#&& &mT***!

&*2....  & & &EE-...!%JJJ&   & &VVM"">b#&& &}d+++!

&*2....  & & &FF=$///!%JJJ&   
" 
"UU=!!>*R"5"5>EE-&&&JJC   	""&r**** " " "mT***!


"	"  
" 
"VVM"">*R"5"5>FF=$'''JJC   	""&r**** " " "}d+++!


"	" 455N !3<QRRR X
WWWW4WWW } }v	779d++  !%% ggdmmVGGFC((	   #  Bvvd||s** $)ByM"*BwK66+.. :*---//;66/22 I ?+ "&J !WW[11N #''*b"9"9"="=f"E"E"O"O!. .~d/CDD" "J  *,;;==DD1111>11111  *i8   D ww}--H %77:r22||F++ "= # 6 6" " "
 /= F

$ & -$2F*F*F%5h%P%PN% -(,% %3M")JGGFCGGD#$6$677!"	   #  B$x3t9,,,:=)%)
 -
 '4X>>Owwz2..H<<''D'88K" 3&&-osCC '0#FF ' ,HcmmooFF KKWc(:(:;;'1133    ' " "d8s4y0008B}-)-J!E 1 ( ,,,#%%1#xEE & ggdmm:%%%(]]__N(+%%%5h%P%PN1  .&M(*@*P*PQQQ!% *)
 '	2 1 1 1,0)))1! "&7&CHY\gHgHgNNA ,,&   5S(CCC)7C&"3K& " "d8s??0>B}-)-J!E +
 c!!
 /x88///S;5F4U4U4W4WZ_4_4_  0#--//JJH 5:  GGFCGGD#,>,>??$!$  " #+ & &B!$x3t9444<= 1-1
 %  5 3444 6>> WWX..F )% &

7 3 3$*JJ{A$>$>	 ,yE?Q?Q  <CGGD"<M<MNN 	) &%@ %(GGFCGGD#4F4F$G$G$-$)!" !" !" !)"KK!? #c0B0B C C ) %   '/ * *#%d8s4y#8#8$,OOB$7$7$7$9$=$=c"T(mm$L$L$L15J$)E	 $9 =SAAA$$ 6>>#&==??+--HHE',C$& " "d8s4y00.3B{O)-J!E 1
 

3 	 	 	@73774==7C   H	  G((=(EFFFFJs   E**"FFG22"HH:J"J43J4L,,"MMl"Cl(Al-AS>=l>TlTBl8FlA2l%_'&l'_=:l<_==G?l>B,l,A#lAm$#m$r   c                      	 ddl m}   |             pi }t          |t                    r|                    di           ni }t          |                    dt                              S # t          $ r
 t          cY S w xY w)zVResolve the per-job output-file retention cap from config (``cron.output_retention``).r   r  r   output_retention)r  r  r   r   r5   r  _CRON_OUTPUT_DEFAULT_KEEPr_   r  s      r   _cron_output_keepr    s    )111111kmm!r*4S$*?*?G37762&&&R8<< 24MNNOOO ) ) )(((()s   A&A) )A=<A=r  keepc                 ^   |dk    rdS 	 t          d |                     d          D             d d          }n# t          $ r Y dS w xY wd}||d         D ]T}	 |                                 |dz  }# t          $ r+}t                              d	|j        |           Y d}~Md}~ww xY w|S )
a  Remove the oldest ``*.md`` run-output files beyond *keep*. Returns count deleted.

    Mirrors the quick-snapshot retention in ``hermes_cli.backup._prune_quick_snapshots``:
    output filenames are timestamp-based (``%Y-%m-%d_%H-%M-%S.md``) so a reverse
    lexical sort orders newest-first, and everything past *keep* is the tail to
    drop. A non-positive *keep* disables pruning. Pruning failures are swallowed
    so they can never break output saving.
    r   c              3   B   K   | ]}|                                 |V  d S N)is_file)r   r  s     r   r   z$_prune_job_output.<locals>.<genexpr>  s/      CC1qyy{{CQCCCCCCr0   z*.mdc                     | j         S r  )r   )r  s    r   <lambda>z#_prune_job_output.<locals>.<lambda>  s    !& r0   T)r   reverseNrg   z"Failed to prune cron output %s: %s)rt  globr~   r  r`   rj  r   )r  r  filesdeletedstaler   s         r   _prune_job_outputr"    s    qyyqCC++F33CCC  
 
 

    qqGtuu P P	PLLNNNqLGG 	P 	P 	PLL=uz3OOOOOOOO	PNs'   0; 
A	A	A55
B*?!B%%B*c                    t                       t          |           }|                    dd           t          |           t	                                          d          }|| dz  }t          j        t          |          dd          \  }}	 t          j
        |dd	
          5 }|                    |           |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          ||           t!          |           n5# t"          $ r( 	 t          j        |           n# t&          $ r Y nw xY w w xY wt)          |t+                                 |S )zSave job output to file.Tr   z%Y-%m-%d_%H-%M-%Sz.mdr{  z.output_r|  r  ri   rj   N)rt   r   r   r   r$  r&  r  r  r   rL   r  rg  r  r  r   r   r   r  r  r~   r"  r  )rY   r#   r  	timestampoutput_filer  r  r  s           r   r  r    s   MMM$V,,N555&&':;;I i#4#4#44K#N(;(;FS]^^^LBYr3111 	!QGGFOOOGGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	x---[!!!!   	Ih 	 	 	D	 n&7&9&9:::sU   D3 ,AD<D3 DD3 D"D3 3
E%>EE%
E E%E  E%rV   c                 "   t          | pd                                          }|sdS 	 ddlm}  ||          p|}n-# t          $ r  t
                              d| d           Y nw xY w|                                                    d          S )u  Reduce one job skill reference to the bare name the curator matches on.

    A job may store an absolute path under ``HERMES_HOME/skills`` or an
    external skills dir; the scheduler resolves those through
    ``normalize_skill_lookup_name`` before handing them to ``skill_view``.
    The curator compares this set against bare skill names, so it has to
    resolve them the same way — otherwise a path-referencing job's skill
    looks unreferenced and gets archived out from under it.

    Best-effort: if the resolver is unavailable or rejects the value, fall
    back to the plain cleanup so a broken import can never lose a name.
    rK   r   )normalize_skill_lookup_namez8referenced_skill_names: could not normalize skill ref %rTr\   r   )r   rN   agent.skill_utilsr'  r_   r`   rj  lstrip)rV   r   r'  s      r   _canonical_skill_refr*     s     	rNN  ""E r
AAAAAA++E22;e 
 
 
F 	 	
 	
 	
 	
 	


 ;;==$$$s   = 'A'&A'c                     	 t                      } n:# t          $ r- t                              dd           t	                      cY S w xY wt	                      }| D ]w}t          |t                    st          |                    d          |                    d                    D ](}t          |          }|r|
                    |           )x|S )u  Return the set of skill names referenced by ANY cron job.

    Includes paused and disabled jobs deliberately: a paused job never
    fires, so its skills never get a ``bump_use`` from the scheduler, yet
    resuming it must still find its skills present. The curator uses this
    set to protect referenced skills from inactivity archival — a skill a
    live job depends on is "in use" regardless of when it was last loaded.

    Names are canonicalized the way the scheduler resolves them at load
    time, so a job that stores an absolute skill path is protected too.

    Best-effort: a corrupt/unreadable jobs store returns an empty set
    rather than raising, so a cron issue can never break the curator.
    z0referenced_skill_names: failed to load cron jobsTr\   r   r   )r   r_   r`   rj  rB   r   r   r   r5   r*  r  )r  namesr   r   cleaneds        r   referenced_skill_namesr.  ;  s    {{   GRVWWWuu eeE # ##t$$ 	)#'''*:*:CGGH<M<MNN 	# 	#D*400G #		'"""	# Ls    4AAconsolidatedprunedc                 6   t          | pi           } t          |pg           }|t          |                                           z  }| s|sg dddS t                      5  t	                      }g }d}|D ]3}t          |                    d          |                    d                    }|s<i }g }	g }
|D ]b}|| v r)| |         }|||<   |r||
vr|
                    |           /||v r|	                    |           I||
vr|
                    |           c|s|	s|
|d<   |
r|
d         nd|d<   d}|                    |                    d          |                    d	          p|                    d          t          |          t          |
          ||	d
           5|r7t          |           t                              dt          |                     |t          |          t          |          dcddd           S # 1 swxY w Y   dS )u	  Rewrite cron job skill references after a curator consolidation pass.

    When the curator consolidates a skill X into umbrella Y (or archives X
    as pruned), any cron job that lists ``X`` in its ``skills`` field will
    fail to load ``X`` at run time — the scheduler logs a warning and
    skips the skill, so the job runs without the instructions it was
    scheduled to follow. See cron/scheduler.py where ``skill_view`` is
    called per skill name.

    This function repairs cron jobs in-place:

    - A skill listed in ``consolidated`` is replaced with its umbrella
      target (the ``into`` value). If the umbrella is already in the
      job's skill list, the stale name is dropped without duplication.
    - A skill listed in ``pruned`` is dropped outright — there is no
      forwarding target.
    - Ordering and other skills in the list are preserved.
    - The legacy ``skill`` field is realigned via ``_apply_skill_fields``.

    Args:
        consolidated: mapping of ``old_skill_name -> umbrella_skill_name``.
        pruned: list of skill names that were archived with no forwarding
            target.

    Returns a report dict::

        {
            "rewrites": [
                {
                    "job_id": ...,
                    "job_name": ...,
                    "before": [...],
                    "after": [...],
                    "mapped": {"old": "new", ...},
                    "dropped": ["old", ...],
                },
                ...
            ],
            "jobs_updated": N,
            "jobs_scanned": M,
        }

    Best-effort: exceptions from loading/saving propagate to the caller so
    tests can assert behaviour; the curator invocation site wraps this
    call in a try/except so a failure here never breaks the curator.
    r   )rewritesjobs_updatedjobs_scannedFr   r   NTr   r   )rY   job_namer   aftermappeddroppedz2Curator rewrote skill references in %d cron job(s))r   rB   keysr   r   r   r5   r   r   r  r`   r  r   )r/  r0  
pruned_setr  r2  changedr   skills_beforer7  r8  
new_skillsr   targets                r   rewrite_skill_refsr?  [  s   d *++LV\r""J #l''))***J F
 F1EEE	 3
 3
{{)+ "	 "	C1#'''2B2BCGGHDUDUVVM  %'F!#G$&J% 	, 	,<'')$/F#)F4L 2&
":":"))&111Z''NN4((((++%%d+++ ' &CM,6@:a==DCLGOO''$--GGFOO<swwt}}}--j)) "       	dOOOKKDc(mm  
 !MMII
 
_3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
 3
s   F"HHHr  )rK   r  )F)r   N)NNNNNNNNNNNNNFNNNN)NNN)r\  
contextlibr  contextvarsr   dataclassesr   rh  loggingr  r  r   rx   rL   r	  r   rw   r   r   r   r   pathlibr   r
  r   typingr	   r
   r   r   r   r   r   r   	getLoggerr+   r`   hermes_timer   r$  utilsr   r   r   r   r   r.   r   r:   r;   r6   r7   TICKER_HEARTBEAT_FILETICKER_SUCCESS_FILETICKER_INTERVAL_SECONDSr   rr   r,  rq   r"   r   Lockr   rz   r8   r>  r'   r3   r9   r?   contextmanagerrE   rG   rS   rU   rO   rP   rX   rb   rd   r   r   r   	frozensetrr  r   r   r   r   r   r   r   r   r   r   r   stat_resultr   rt   r  r  r-  r2  r7  r9  r@  r   rH  rI  re  rK  r   rS  rB  rV  rm  rq  rv  ry  r  r  r  r  r  r  r  r  r  r  r  r   r  r  r  r  r  r  r  r  r  r  r  r  r#  rL  rO  LookupErrorrQ  rd  rn  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r"  r  r*  r.  r?  r/   r0   r   <module>rQ     s?          " " " " " " ! ! ! ! ! !          				 				 LLLL   EEEMMMM   FFF ( ( ( ( ( ( ( (       , , , , , , K K K K K K K K K K K K K K K K K K K K		8	$	$ * * * * * * 3 3 3 3 3 3 3 3 #htn # # #
$ 
 
 
 
8 _&&((
 {"	 !#55  !66 
  
 ")/##"9?$$ 02 4Y_,- 2 2 2().**  "  
  $        ?Ij? ? ? j/!:;     )Z@@R_ R R R R> *sDy) * * * * ,T , , , , !%  #$ #(      :     49 9 9 9 9
 e/ e/ e/P <$3 <$ <$ <$ <$~ S S    $ "	4&)) 3C 3D 3 3 3 3  # x} X\]`Xa    "T#s(^ S#X     C 3     4S> c    " tCH~  $sCx.        F&4S> &d & & & &c3h D    !T#s(^ ! ! ! ! !(d    t    #
4 #
".1I #
d #
 #
 #
 #
L" " "%c %c % % % %*aS aT#s(^ a a a aH$h $8 $ $ $ $&	5h 	5 	5d 	5 	5 	5 	5
F 
FH 
F 
F 
F 
F 
F" "&	  38n	 #	
 c]   <1T 1c 1 1 1 1: $% S $ $ $$& !+- "D - - -+N	c3h+N38n+N 
+N 
	+N +N +N +N\.S#X .8E? . . . .d 35 T#x./ 4 4 4R$sCx. RS RUY R R R R,DcN    ; ;tCH~ ;HSM ;U]^aUb ; ; ; ;D	Ad 	At 	A 	A 	A 	A S T    $ T d    8$ 8E?    @(5/ @ @ @ @C C C C C
 
 
 
           Fs       x}    $3 3sDy)9 3 3 3 3",4S#X' , , , ,^Xd4S>&:;    2 %S#2F)G    &(huS#s]';< ( ( ( ( (( .2< < <
tCH~
< *S/*< 
$sCx.	< < < <D .2	C C C
tCH~
C *S/*C 	C C C CR .2	L L L
tCH~
L *S/*L 	L L L L (3-    B)# ) ) ) )X NS    d W_`cWd     s  x}        F*-*- *- 	*-
 *- 8C=(3-'(*- *- *- *-ZDcN uXc]HUXM[cdg[hjn=n7o    
SM
#
 
 SM	

 

 
 
 
B  !'+"&"" 48,0!(,$(!%&*)p pSMpp 3-p SM	p
 c]p T#s(^$p C=p T#Yp C=p smp smp SMp 5d3i01p tCy)p c]p  !p"  ~#p$ SM%p& #'p( sm)p* 
#s(^+p p p pfC HT#s(^4    

 

 

 

 

K 

 

 

2 2$sCx.!9 2 2 2 22  d38n1E     Ds DT#s(^ Dc3h8P D D D DN c 8C= HT#s(^<T     s xS#X7    2 c3h 8    "$s $t $ $ $ $T  $( 
 *.
 
 


 C=
 SM	

 SM
 "#
 

 
 
 
,C  D T    4?3 ?t ? ? ? ? ?
03 04 0 0 0 0
*C *D * * * *
:s :t : : : :
4 4 4 4 4 4
c 3 4    F  $(	S !)-S S SSS C=S SM	S SMS "#S 
S S S Sl)
$sCx. )
T )
 )
 )
 )
XO3 O4 O O O Od      @C D    8"# " " " "J,S ,T , , , ,"#S # # # #( !
 
 

 
 	

 
 4c3h 
 
 
 
* !J J JJ J 	J
 J 4c3h J J J J` $%  75 7 7 7 70 '+	9 9 94S>"9	9 #c(#	9
 
9 9 9 9x
 
 
 
 
 
 
      .&d4S>* & & & &$qd4S>2 q q q qr  )3 ) ) ) )d # #    :C     F%c %c % % % %6C    B .2"&n
 n
4S>*n
T#Yn
 
#s(^n
 n
 n
 n
 n
 n
s#   A AAA AA