
    iԚ              %          d 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	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  ej        e          ZddlmZ ddlmZ 	 dd	lmZ d
Zn# e$ r dZY nw xY w e                                             Z!e!dz  Z"e"dz  Z# ej$                    Z%e"dz  Z&dZ'dNdee(         dee         dee(         fdZ)dee(ef         dee(ef         fdZ*defdZ+defdZ,d Z-de(de.fdZ/de(dee(ef         fdZ0de	de	fdZ1dd dee(ef         d!e	d"ee(         dee(         fd#Z2de3de.fd$Z4dOdee(ef         d"ee(         dee(         fd%Z5deee(ef                  fd&Z6d'eee(ef                  fd(Z7d)ee(         dee(         fd*Z8	 	 	 	 	 	 	 	 	 	 	 	 	 	 dPd+ee(         de(d,ee(         d-ee.         d.ee(         d/eee(ef                  dee(         deee(                  d0ee(         d1ee(         d2ee(         d3ee(         d4eee(ee(         f                  d5eee(                  d)ee(         d6e9dee(ef         f"d7Z:d8e(deee(ef                  fd9Z;dQd:e9deee(ef                  fd;Z<d8e(d<ee(ef         deee(ef                  fd=Z=dOd8e(d>ee(         deee(ef                  fd?Z>d8e(deee(ef                  fd@Z?d8e(deee(ef                  fdAZ@d8e(de9fdBZA	 	 dNd8e(dCe9dDee(         dEee(         fdFZBd8e(de9fdGZCdeee(ef                  fdHZDdeee(ef                  fdIZEd8e(de(fdJZF	 	 dNdKeee(e(f                  dLeee(                  dee(ef         fdMZGdS )Rz
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)datetime	timedelta)Path)get_hermes_home)OptionalDictListAnyUnion)now)atomic_replace)croniterTFcronz	jobs.jsonoutputx   skillskillsreturnc                     || 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.N )
isinstancestrliststripappend)r   r   	raw_items
normalizeditemtexts         ./home/piyush/.hermes/hermes-agent/cron/jobs.py_normalize_skill_listr!   0   s    ~$,UGG"			FC	 	  !H		LL	J $ $4:2$$&& 	$D
**d###    jobc                     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)dictr!   get)r#   r   r   s      r    _apply_skill_fieldsr'   A   s[    cJ":>>'#:#:JNN8<T<TUUF!Jx'-7&))4Jwr"   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)oschmodOSErrorNotImplementedErrorr(   s    r    _secure_dirr/   J   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)existsr*   r+   r,   r-   r.   s    r    _secure_filer2   R   sa    ;;== 	"HT5!!!!!	" 	"()   s   )/ AAc                      t                               dd           t                              dd           t          t                      t          t                     dS )z6Ensure cron directories exist with secure permissions.Tparentsexist_okN)CRON_DIRmkdir
OUTPUT_DIRr/    r"   r    ensure_dirsr;   [   sS    NN4$N///TD111
r"   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'      r   <   i  )mhd)r   lowerrematch
ValueErrorintgroup)r<   rF   valueunitmultiplierss        r    parse_durationrM   g   s     	
		AH\^_``E [YqYYYZZZAE;;q>>!D..K;t$$$r"   schedulec                 `   |                                  } | }|                                 }|                    d          r5| dd                                          }t          |          }d|d| ddS |                                 }t          |          dk    rut          d |dd         D                       rTt          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        |                                }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intervalrA   )kindminutesdisplay   c              3   @   K   | ]}t          j        d |          V  dS )z^[\d\*\-,/]+$N)rE   rF   ).0ps     r    	<genexpr>z!parse_schedule.<locals>.<genexpr>   s@        *+!1%%     r"   zOCron expressions require 'croniter' package. Install with: pip install croniterzInvalid cron expression 'z': r   )rR   exprrT   Tz^\d{4}-\d{2}-\d{2}Zz+00:00oncezonce at z%Y-%m-%d %H:%M)rR   run_atrT   zInvalid timestamp 'rS   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))r   rD   
startswithrM   splitlenallHAS_CRONITERrG   r   	ExceptionrE   rF   r   fromisoformatreplacetzinfo
astimezone	isoformatstrftime_hermes_nowr   )	rN   originalschedule_lowerduration_strrS   partsedtr^   s	            r    parse_schedulers   |   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 ]]__,,..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.DD+A0F 
G &F;;G AH 
HHrr   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.
    N)rh   )rl   rh   r   r   ri   rg   )rr   	target_tzlocal_tzs      r    _ensure_awarerw      sf     $I	y<>>,,..5zzz**55i@@@==###r"   last_run_atr   ry   c                    |                      d          dk    rdS |rdS |                      d          }|sdS t          t          j        |                    }||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.
    rR   r]   Nr^   )seconds)r&   rw   r   rf   r   ONESHOT_GRACE_SECONDS)rN   r   ry   r^   	run_at_dts        r    _recoverable_oneshot_run_atr~      s     ||Fv%%t t\\(##F th4V<<==IC),ABBBBBB4r"   c                 6   d}d}|                      d          }|dk    r<|                      dd          dz  }|dz  }t          |t          ||                    S |d	k    rt          r	 t	                      }t          | d
         |          }|                    t                    }|                    t                    }	t          |	|z
  	                                          }|dz  }t          |t          ||                    S # t          $ r Y nw xY w|S )a(  Compute how late a job can be and still catch up instead of fast-forwarding.

    Uses half the schedule period, 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   rR   rQ   rS   r>   r@   r?   r   rZ   )r&   maxminrd   rl   r   get_nextr   rH   total_secondsre   )
rN   	MIN_GRACE	MAX_GRACErR   period_secondsgracer   r   firstseconds
             r    _compute_grace_secondsr     s     II<<Dz!i33b8!#9c%33444v~~,~		--CHV,c22DMM(++E]]8,,F &5.!?!?!A!ABBN"a'Ey#eY"7"7888 	 	 	D	 s   *BD	 	
DDc                    t                      }| d         dk    rt          | ||          S | d         dk    rf| d         }|r5t          t          j        |                    }|t          |          z   }n|t          |          z   }|                                S | d         dk    rt          s0t          	                    d| 
                    d	                     d
S |}|r!t          t          j        |                    }t          | d	         |          }|                    t                    }|                                S d
S )zo
    Compute the next run time for a schedule.

    Returns ISO timestamp string, or None if no more runs.
    rR   r]   rx   rQ   rS   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.rZ   N)rl   r~   rw   r   rf   r   rj   rd   loggerwarningr&   r   r   )rN   ry   r   rS   lastnext_run	base_timer   s           r    compute_next_runr   #  s[    --C6!!*8SkRRRR	&	Z	'	'9% 	8 !7!D!DEEDi8888HH Yw7777H!!###	&	V	#	# 	NN V$$   4
 	 	K%h&<[&I&IJJI()44==**!!###4r"   c                  h   t                       t                                          sg S 	 t          t          dd          5 } t	          j        |           }|                    dg           cddd           S # 1 swxY w Y   dS # t          j        $ r 	 t          t          dd          5 } t	          j        | 	                                d          }|                    dg           }|r)t          |           t                              d           |cddd           cY S # 1 swxY w Y   Y dS # t          $ r3}t                              d	|           t          d
|           |d}~ww xY wt           $ r3}t                              d|           t          d|           |d}~ww xY w)zLoad all jobs from storage.rutf-8encodingjobsNF)strictz8Auto-repaired jobs.json (had invalid control characters)z#Failed to auto-repair jobs.json: %sz*Cron database corrupted and unrepairable: zIOError reading jobs.json: %szFailed to read cron database: )r;   	JOBS_FILEr1   openjsonloadr&   JSONDecodeErrorloadsread	save_jobsr   r   re   errorRuntimeErrorIOError)fdatar   rq   s       r    	load_jobsr   U  sc   MMM 	H)S7333 	(q9Q<<D88FB''	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(  X X X	Xiw777 1z!&&((5999xx++ _dOOONN#]^^^                      	X 	X 	XLL>BBBOAOOPPVWW	X  H H H4a888?A??@@aGHs   B *A9,B 9A==B  A=B F1D5-A+D'D5$F1'D+	+D5.D+	/D55
E2?.E--E22F1>.F,,F1r   c                    t                       t          j        t          t          j                  dd          \  }}	 t          j        |dd          5 }t          j	        | t                                                      d|d	           |                                 t          j        |                                           d
d
d
           n# 1 swxY w Y   t          |t                     t!          t                     d
S # t"          $ r( 	 t          j        |           n# t&          $ r Y nw xY w w xY w)zSave all jobs to storage..tmpz.jobs_dirsuffixprefixwr   r   )r   
updated_atr?   )indentN)r;   tempfilemkstempr   r   parentr*   fdopenr   dumprl   rj   flushfsyncfilenor   r2   BaseExceptionunlinkr,   )r   fdtmp_pathr   s       r    r   r   r  sq   MMM#I,<(=(=fU]^^^LBYr3111 	!QIt;==3J3J3L3LMMqYZ[[[[GGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	x+++Y   	Ih 	 	 	D	sU   D A3CD CD C,D 
D?D-,D?-
D:7D?9D::D?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   r   r   
expanduseris_absoluterG   resolver1   is_dir)r   rawexpandedresolveds       r    _normalize_workdirr     s     t
g,,



C tCyy##%%H!! 
[# [ [ [
 
 	
 !!H?? ECCCDDD?? IGXGGHHHx==r"   promptnamerepeatdeliveroriginmodelproviderbase_urlscriptcontext_fromenabled_toolsetsno_agentc                    t          |          }||dk    rd}|d         dk    r|d}||rdnd}t          j                    j        dd         }t	                                                      }t          ||          }t          |t                    r!t          |          	                                nd}t          |	t                    r!t          |	          	                                nd}t          |
t                    r4t          |
          	                                
                    d	          nd}|pd}|pd}|pd}t          |t                    r!t          |          	                                nd}|pd}|rd
 |D             nd}|pd}t          |          }t          |          }|r|st          d          t          |t                    r,|	                                r|	                                gnd}n&t          |t                    rd |D             pd}nd}| p|r|d         ndp|r|ndp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d d!d"d#dd|t!          |          dddd||||d$}t#                      }|                    |           t'          |           |S )%uX  
    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.

    Returns:
        The created job dict
    Nr   rR   r]   r>   r   local   /c                     g | ]D}t          |                                          #t          |                                          ES r:   r   r   )rW   ts     r    
<listcomp>zcreate_job.<locals>.<listcomp>  s9    VVVas1vv||~~V3q66<<>>VVVr"   ud   no_agent=True requires a script — with no agent and no script there is nothing for the job to run.c                     g | ]D}t          |                                          #t          |                                          ES r:   r   rW   js     r    r   zcreate_job.<locals>.<listcomp>  s9    OOO1AOAOOOr"   zcron jobidr   2   r   r   r   r   r   r   r   r   r   rN   schedule_displayrT   r   )times	completedenabledTstate	scheduled	paused_at)paused_reason
created_atnext_run_atry   last_status
last_errorlast_delivery_errorr   r   r   r   )rs   uuiduuid4hexrl   rj   r!   r   r   r   rstripr   boolrG   r   r&   r   r   r   r   )r   rN   r   r   r   r   r   r   r   r   r   r   r   r   r   r   parsed_schedulejob_idr   normalized_skillsnormalized_modelnormalized_providernormalized_base_urlnormalized_scriptnormalized_toolsetsnormalized_workdirnormalized_no_agentlabel_sourcer#   r   s                                 r    
create_jobr     s4   B %X..O fkk v&((V^ $1(('Z\\crc"F
--
!
!
#
#C-eV<<-7s-C-CMs5zz'')))3=h3L3LV#h----///RV?I(TW?X?Xb#h----//66s;;;^b'/4-5-5/9&#/F/FPF))+++D)1TZjtVV3CVVVVpt-5+G44x..
  
#4 
3
 
 	
 ,$$ 1=1C1C1E1EO**,,--4	L$	'	' OOOOOWSW  L7HR033d  L  nA  YKXiXi  GK  [  Q[L!f!1SbS)//11! 	&! 	#	!
 	):D"1%%! 	!! 	'! 	'! 	#! 	'! 	! 	O! 	O//	8DD! 	
 
!$ 	4%!& 	'!( 	T)!* '88#/%A! ! !CF ;;DKKdOOOJr"   r   c                 f    t                      }|D ]}|d         | k    rt          |          c S  dS )zGet a job by ID.r   N)r   r'   )r   r   r#   s      r    get_jobr   B  sG    ;;D , ,t9&s+++++ 4r"   include_disabledc                 R    d t                      D             }| sd |D             }|S )z2List all jobs, optionally including disabled ones.c                 ,    g | ]}t          |          S r:   r'   r   s     r    r   zlist_jobs.<locals>.<listcomp>M  s!    888q""888r"   c                 >    g | ]}|                     d d          |S )r   T)r&   r   s     r    r   zlist_jobs.<locals>.<listcomp>O  s+    :::a155D#9#9::::r"   )r   )r   r   s     r    	list_jobsr  K  s9    88IKK888D ;::4:::Kr"   updatesc           
         t                      }t          |          D ]\  }}|d         | k    rd|v r$|d         }|dv rd|d<   nt          |          |d<   t          i ||          }d|v }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    rt          |	          |d<   |                    dd          rF|                    d          dk    r-|                    d          st          |d                   |d<   |||<   t          |           t          ||                   c S dS )zCUpdate a job by ID, refreshing derived schedule fields when needed.r   r   )Nr   FNrN   r   r   r   r   rT   r   pausedr   r   T)r   	enumerater   r'   r!   r&   r   r   rs   r   r   )
r   r  r   ir#   _wdupdatedschedule_changedr   updated_schedules
             r    
update_jobr  S  s1   ;;DD// ), ),3t9 )$C'''%)	""%7%<%<	"%&8&8&899%0w'W"4"4 5gkk'6J6JGKKX`LaLa b b 1GH7HR033dGG 	L&z2 *C00 7#12B#C#C &6
#*1++" $$Y<N0O0OPP+ +G&' {{7##x//)9:J)K)K&;;y$'' 	KGKK,@,@H,L,LU\U`U`anUoUo,L%5gj6I%J%JGM"Q$"47+++++4r"   reasonc                 h    t          | ddt                                                      |d          S )z Pause a job without deleting it.Fr  )r   r   r   r   )r  rl   rj   )r   r  s     r    	pause_jobr    s=    $0022#		
 	
  r"   c           	      ~    t          |           }|sdS t          |d                   }t          | dddd|d          S )z=Resume a paused job and compute the next future run from now.NrN   Tr   r   r   r   r   r   )r   r   r  )r   r#   r   s      r    
resume_jobr    sY    
&//C t"3z?33K !&	
 	
	 	 	r"   c           	          t          |           }|sdS t          | ddddt                                                      d          S )z1Schedule a job to run on the next scheduler tick.NTr   r  )r   r  rl   rj   )r   r#   s     r    trigger_jobr    sX    
&//C t !&==2244	
 	
	 	 	r"   c                      t                      }t          |          } fd|D             }t          |          |k     rt          |           dS dS )zRemove a job by ID.c                 ,    g | ]}|d          k    |S )r   r:   )rW   r   r   s     r    r   zremove_job.<locals>.<listcomp>  s'    111!qw&00A000r"   TF)r   rb   r   )r   r   original_lens   `  r    
remove_jobr    sV    ;;Dt99L1111t111D
4yy<$t5r"   successr   delivery_errorc           
      x   t           5  t                      }t          |          D ]\  }}|d         | k    rt                                                      }||d<   |rdnd|d<   |s|nd|d<   ||d<   |                    d	          r|d	                             d
d          dz   |d	         d
<   |d	                             d          }|d	         d
         }	|>|dk    r8|	|k    r2|                    |           t          |            ddd           dS t          |d         |          |d<   |d         |                    di                               d          }
|
dv rVd|d<   |                    d          sd|d<   t          
                    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 )uK  
    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).
    r   ry   okr   r   Nr   r   r   r   r   r>   r   rN   r   rR   r   rQ   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   Fr   r  r   z0mark_job_run: job_id %s not found, skipping save)_jobs_file_lockr   r  rl   rj   r&   popr   r   r   r   r   )r   r  r   r  r   r	  r#   r   r   r   rR   s              r    mark_job_runr"    s    
 ;S ;S{{oo 7	 7	FAs4yF""!mm--//%(M"-4%ATT'M"18$BEEdL!-;)* 778$$ 
14X1B1B;PQ1R1RUV1VCM+.  M--g66E #Hk :I(UQYY9;M;M!$-;S ;S ;S ;S ;S ;S ;S ;S2 &6c*os%K%KM" }%-77:r2266v>>D333'.G"ww|44 !J  -
 <  GGFCI66     */I'2GWWW%%11#.CL$s;S ;S ;S ;S ;S ;S ;S ;S #p 	I6RRRw;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;S ;Ss   DH/CH/H//H36H3c                    t           5  t                      }|D ]}|d         | k    r|                    di                               d          }|dvr ddd           dS t                                                      }t          |d         |          }|r;||                    d          k    r"||d<   t          |            ddd           dS  ddd           dS 	 ddd           dS # 1 swxY w Y   d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.
    r   rN   rR   r  NFr   T)r   r   r&   rl   rj   r   r   )r   r   r#   rR   r   new_nexts         r    advance_next_runr%    s    
  {{ 	 	C4yF""wwz2..226::333         "mm--//+C
OSAA  CGGM,B,B B B)1C&dOOO                #                  s%   AC2"A&C2C2#C22C69C6c                  `    t           5  t                      cddd           S # 1 swxY w Y   dS )aO  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),
    the job is fast-forwarded to the next future run instead of firing
    immediately.  This prevents a burst of missed jobs on gateway restart.
    N)r   _get_due_jobs_lockedr:   r"   r    get_due_jobsr(  %  sw     
 & &#%%& & & & & & & & & & & & & & & & & &s   #''c            	         t                      } t                      }d t          j        |          D             }g }d}|D ]'}|                    dd          s|                    d          }|s|                    di           }|                    d          }t          || |                    d          	          }	|	rd
nd}
|	s*|dv r&t          ||                                           }	|	r|}
|	s|	|d<   |	}t          	                    d|                    d|d                   |
|	           |D ]}|d         |d         k    r	|	|d<   d} nt          t          j        |                    }|| k    r|                    di           }|                    d          }t          |          }|dv r| |z
                                  |k    r~t          ||                                           }|rZt          	                    d|                    d|d                   |||           |D ]}|d         |d         k    r	||d<   d} n|                    |           )|rt!          |           |S )zQInner implementation of get_due_jobs(); must be called with _jobs_file_lock held.c                 ,    g | ]}t          |          S r:   r  r   s     r    r   z(_get_due_jobs_locked.<locals>.<listcomp>5  s!    DDDq""DDDr"   Fr   Tr   rN   rR   ry   rx   zone-shotNr  z4Job '%s' had no next_run_at; recovering %s run at %sr   r   zSJob '%s' missed its scheduled time (%s, grace=%ds). Fast-forwarding to next run: %s)rl   r   copydeepcopyr&   r~   r   rj   r   inforw   r   rf   r   r   r   r   )r   raw_jobsr   due
needs_saver#   r   rN   rR   recovered_nextrecovery_kindrjnext_run_dtr   r$  s                  r    r'  r'  1  s   
--C{{HDDDM(,C,CDDDD
CJ J Jwwy$'' 	77=)) %	wwz2..H<<''D 9GGM22  N
 +9BJJdM " )d.B&B&B!1(CMMOO!L!L! )$(M! !/C%HKKFD	**	     d8s4y(((6B}%!%JE )
 $H$:8$D$DEE#wwz2..H<<''D
 +844E+++{1B0Q0Q0S0SV[0[0[ ,HcmmooFF KK:D	22     ' " "d8s4y0008B}-)-J!E 1 JJsOOO (Jr"   c                    t                       t          | z  }|                    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|S )zSave job output to file.Tr4   z%Y-%m-%d_%H-%M-%Sz.mdr   z.output_r   r   r   r   N)r;   r9   r8   r/   rl   rk   r   r   r   r*   r   writer   r   r   r   r2   r   r   r,   )r   r   job_output_dir	timestampoutput_filer   r   r   s           r    save_job_outputr:    s   MMM&(N555&&':;;I i#4#4#44K#N(;(;FS]^^^LBYr3111 	!QGGFOOOGGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	x---[!!!!   	Ih 	 	 	D	 sU   D. 'AD7D. DD. 
D"D. .
E 9EE 
EE EE consolidatedprunedc                 (   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   )r   job_namebeforeaftermappeddroppedz2Curator rewrote skill references in %d cron job(s))r%   setkeysr   r   r!   r&   r   r   r   r   r-  rb   )r;  r<  
pruned_setr   r>  changedr#   skills_beforerD  rE  
new_skillsr   targets                r    rewrite_skill_refsrM    s   d *++LV\r""J #l''))***J F
 F1EEE	 4
 4
{{)+ #	 #	C1#'''2B2BCGGHDUDUVVM  %'F!#G$&J% 
0 
0<'')$/F#)F4L 2&
":":"))&111Z''NN4((((:--"))$/// ' &CM,6@:a==DCLGOO''$--GGFOO<swwt}}}--j)) "       	dOOOKKDc(mm  
 !MMII
 
a4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
 4
s   F"HHH)NN)N)NNNNNNNNNNNNNF)F)H__doc__r+  r   loggingr   	threadingr*   rE   r   r   r   pathlibr   hermes_constantsr   typingr   r   r	   r
   r   	getLogger__name__r   hermes_timer   rl   utilsr   r   rd   ImportErrorr   
HERMES_DIRr7   r   Lockr   r9   r|   r   r!   r'   r/   r2   r;   rH   rM   rs   rw   r~   r%   r   r   r   r   r   r   r   r   r  r  r  r  r  r  r"  r%  r(  r'  r:  rM  r:   r"   r    <module>r[     s            				 				  ( ( ( ( ( ( ( (       , , , , , , 3 3 3 3 3 3 3 3 3 3 3 3 3 3		8	$	$ * * * * * *            !!!!!!LL   LLL _&&((
{"	
 !)."" 
  # x} X\]`Xa    "T#s(^ S#X    d    t      %c %c % % % %*VS VT#s(^ V V V Vr$h $8 $ $ $ $. "&	  38n	 #	
 c]   6T c    @+ +tCH~ +HSM +U]^aUb + + + +dH4S#X' H H H H:Dc3h(    & (3-    H  !'+"&"" 48,0!!Y YSMYY 3-Y SM	Y
 c]Y T#s(^$Y C=Y T#YY C=Y smY smY SMY 5d3i01Y tCy)Y c]Y  !Y" 
#s(^#Y Y Y YxC HT#s(^4      d38n1E    -s -T#s(^ -c3h8P - - - -`
 
c 
8C= 
HT#s(^<T 
 
 
 
s xS#X7    & c3h 8    "s t     EI15FS FS FSt FSHSM FS!)#FS FS FS FSRS T    :	&d4S>* 	& 	& 	& 	&Wd4S>2 W W W WtC     B .2"&o
 o
4S>*o
T#Yo
 
#s(^o
 o
 o
 o
 o
 o
s   "A+ +A54A5