
    Jj3                    D	   U 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
Z
ddlZddlZddlZ	 ddlZn # e$ r dZ	 ddlZn# e$ r dZY nw xY wY nw xY wddlmZ ddlmZmZmZ ej                            d e ee          j        j                             ddlmZ ddlmZ ddl m!Z!m"Z" ddl#m$Z$ dd	l%m&Z'  ej(        e)          Z*d
e+dedz  defdZ, G d de-          Z.de+de/e         fdZ0de/e         de+de/e         fdZ1d
e+de+de/e         dz  fdZ2 e3h d          Z4i ddddddddddd d!d"d#d$d%d&d'd(d)d*d+d,d-d.d/d0d1d2d3d4d5Z5d1d6iZ6dd7l7m8Z8m9Z9m:Z:m;Z;m<Z< d8Z= e3h d9          Z>d:ede?fd;Z@daAeejB        jC                 eDd<<   daEeeF         eDd=<    eG            ZHeGeDd><    ejI                    ZJdaKeejB        jC                 eDd?<    G d@ dA          ZL eL            ZMdBeeF         dejB        jC        fdCZNdejB        jC        fdDZOddEZP ejQ        eP           ddFeeR         de?fdGZSdZTedz  eDdH<   defdIZUdeVeef         fdJZWd
e+dee+         fdKZXdd
e+dee+         de?fdLZYdMe+dNedOedPee         de?f
dQZZ	 	 ddRdSd
e+dNedOedTedPee         dUee         dVe?ddfdWZ[d
e+dOedee         fdXZ\	 dd
e+dNedOedPedTedYee         ddfdZZ]dd[d
e+dNedOedTed\e?dUee         dYee         de?fd]Z^d
e+defd^Z_dNedefd_Z`dNede?fd`ZadNedefdaZbdNedefdbZcdNedee         fdcZddd Zede/e+         fdeZfd
e+dfedee+         fdgZgdefdhZh e3dih          Zidjedee         fdkZjd
e+dee+         fdlZkd
e+dee+         fdmZl e3h dn          Zm e3h do          Zn	 ddOedpe/dqe+dz  d
e+ddf
drZode?fdsZpdtedOeduedvede?f
dwZqdd
e+dxedee         fdyZrdzZsesZtdeFfd{Zud|edeVe?ef         fd}Zvd~ede?fdZwdd
e+deeV         defdZxdRdRddded
e+de?de?dee         defdZyd
e+ddfdZzddd
e+dee/         deVe?eeee         f         fdZ{dveddfdZ|dddRdd
e+de?de?fdZ}ddZ~dde?de?deFfdZe)dk    r ed           dS dS )a  
Cron job scheduler - executes due jobs.

Provides tick() which checks for due jobs and runs them. The gateway
calls this every 60 seconds from a background thread.

Uses a file-based lock (~/.hermes/cron/.tick.lock) so only one tick
runs at a time if multiple processes overlap.
    NPath)AnyListOptional)get_hermes_home)windows_hide_flags)load_config_expand_env_vars)get_fallback_chain)nowjoberrorreturnc                 z   |                      d          p|                      d          pd}|pd                                }|                                }d|v sd|v sd|v rd}d|v rd}nd	|v rd
}d| d| dS d|v sd|v sd|v rd| dS t          j        d|          st          j        d|          rd| dS t          j        dd|dd                   }t          j        dd|                                          }t          |          dk    r|dd                                         dz   }d| d| S )a  Return a compact one-line failure message for chat delivery.

    Full details stay in the cron output directory and the logs. Chat should
    show the operator what broke without dumping provider JSON, retry noise, or
    stack traces into the delivery channel.
    nameidcron jobzunknown error429z
rate limitzusage limitzweekly usage limitquotazquota limitu   ⚠️ Cron 'z' failed: provider zQ. Fallback chain was exhausted or unavailable. Full details saved in cron output.readtimeoutz	timed outtimeoutzk' failed: provider timeout. Fallback chain was exhausted or unavailable. Full details saved in cron output.zauthenticat|authorizz\b(401|403)\bzK' failed: provider authentication error. Full details saved in cron output.z8^(RuntimeError|Exception|ValueError|HTTPStatusError):\s* Ni  z\s+       z...z
' failed: )getstriplowerresearchsublenrstrip)r   r   job_nametextr   reasoncleaneds          4/home/rurouni/.hermes/hermes-agent/cron/scheduler.py$_summarize_cron_failure_for_deliveryr*   2   s    wwv=#''$--=:H$_++--DJJLLE }}--%1G1G5(()FF"F1H 1 1 1 1 1	
 !5!5e9K9K1H 1 1 1	
 
y(%00 
BI>NPT4U4U 
1H 1 1 1	
 fC
D$K G fVS'**0022G
7||c$3$-&&((508888w888    c                       e Zd ZdZdS )CronPromptInjectionBlockeda  Raised by _build_job_prompt when the fully-assembled prompt trips the
    injection scanner. Caught in run_job so the operator sees a clean
    "job blocked" delivery instead of the scheduler crashing.

    Assembled-prompt scanning (including loaded skill content) plugs the
    gap from #3968: create-time scanning only covers the user-supplied
    prompt field; skill content loaded at runtime was never scanned, so a
    malicious skill could carry an injection payload that reached the
    non-interactive (auto-approve) cron agent.
    N)__name__
__module____qualname____doc__ r+   r)   r-   r-   g   s        	 	 	 	r+   r-   cfgc                     g d}| pi                      d          pi }|                     d          pg }|D ]>}t          |                                          }|r||vr|                    |           ?|S )uX  Toolsets a cron-spawned agent must never receive.

    Three protected toolsets are always disabled in cron context:
      - ``cronjob`` — would let a cron-spawned agent schedule more cron jobs
      - ``messaging`` — interactive, needs a live gateway session
      - ``clarify`` — interactive, blocks waiting for user input

    User-level ``agent.disabled_toolsets`` from config.yaml is layered on top
    so per-job ``enabled_toolsets`` cannot bypass policy that applies to
    ordinary agent runs (#25752 — LLM-supplied enabled_toolsets was widening
    past config.yaml's denylist).
    )cronjob	messagingclarifyagentdisabled_toolsets)r   strr   append)r3   disabled	agent_cfguser_disabledr   s        r)   _resolve_cron_disabled_toolsetsr?   t   s     322H((.BIMM"566<"M " "4yy   	"D((OOD!!!Or+   per_jobc                     d | D             }d| v r|S ddl m}  ||          }t          |          |z  r|S t          |          D ]}||vr|                    |           |S )a  Layer enabled MCP servers onto a per-job ``enabled_toolsets`` allowlist.

    A per-job list scopes the *native* toolsets, but on its own it silently
    drops every MCP server: ``discover_mcp_tools()`` registers the tools into
    the global registry, yet ``get_tool_definitions(enabled_toolsets=...)``
    only keeps toolsets named in the list. The agent then rejects every
    ``mcp_*`` call with "Unknown tool". This restores parity with
    ``_get_platform_tools`` MCP semantics:

      * ``no_mcp`` sentinel present  -> no MCP servers (sentinel stripped)
      * one or more MCP server names already listed -> treat as an allowlist,
        add nothing further (the user named exactly the servers they want)
      * otherwise -> union in every globally-enabled MCP server
    c                     g | ]
}|d k    |S )no_mcpr2   ).0ts     r)   
<listcomp>z4_merge_mcp_into_per_job_toolsets.<locals>.<listcomp>   s    222AAMMaMMMr+   rC   r   )enabled_mcp_server_names)hermes_cli.tools_configrG   setsortedr;   )r@   r3   resultrG   enabled_mcpr   s         r)    _merge_mcp_into_per_job_toolsetsrM      s     32222F7 A@@@@@**3//K
6{{[  {##    vMM$Mr+   c                    |                      d          }|rt          t          |          |pi           S 	 ddlm} t           ||pi d                    S # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)u  Resolve the toolset list for a cron job.

    Precedence:
    1. Per-job ``enabled_toolsets`` (set via ``cronjob`` tool on create/update).
       Keeps the agent's job-scoped toolset override intact — #6130. Enabled
       MCP servers are layered on per ``_merge_mcp_into_per_job_toolsets`` so a
       native-toolset allowlist does not silently strip MCP tools.
    2. Per-platform ``hermes tools`` config for the ``cron`` platform.
       Mirrors gateway behavior (``_get_platform_tools(cfg, platform_key)``)
       so users can gate cron toolsets globally without recreating every job.
    3. ``None`` on any lookup failure — AIAgent loads the full default set
       (legacy behavior before this change, preserved as the safety net).

    _DEFAULT_OFF_TOOLSETS ({moa, homeassistant, rl}) are removed by
    ``_get_platform_tools`` for unconfigured platforms, so fresh installs
    get cron WITHOUT ``moa`` by default (issue reported by Norbert —
    surprise $4.63 run).
    enabled_toolsetsr   )_get_platform_toolscronzHCron toolset resolution failed, falling back to full default toolset: %sN)	r   rM   listrH   rP   rJ   	Exceptionloggerwarning)r   r3   r@   rP   excs        r)   _resolve_cron_enabled_toolsetsrW      s    & gg())G J/WsybIII??????))#)V<<===   V	
 	
 	
 ttttts    A 
B	#BB	>   smsemailqqbotslackwecomfeishumatrixsignalweixindiscordwebhookyuanbaodingtalktelegramwhatsapp
mattermostbluebubbleshomeassistantwecom_callbackr^   MATRIX_HOME_ROOMre   TELEGRAM_HOME_CHANNELra   DISCORD_HOME_CHANNELr[   SLACK_HOME_CHANNELr_   SIGNAL_HOME_CHANNELrg   MATTERMOST_HOME_CHANNELrX   SMS_HOME_CHANNELrY   EMAIL_HOME_ADDRESSrd   DINGTALK_HOME_CHANNELr]   FEISHU_HOME_CHANNELr\   WECOM_HOME_CHANNELr`   WEIXIN_HOME_CHANNELrh   BLUEBUBBLES_HOME_CHANNELrZ   QQBOT_HOME_CHANNELrf   WHATSAPP_HOME_CHANNELwhatsapp_cloudWHATSAPP_CLOUD_HOME_CHANNELQQ_HOME_CHANNEL)get_due_jobsmark_job_runsave_job_outputadvance_next_runclaim_dispatch[SILENT]>   NO REPLYSILENTNO_REPLYr   r&   c                    t          | t                    sdS |                                 }|sdS dt          dt          fd} ||          rdS d |                                D             }|r$ ||d                   s ||d                   rdS |                                }|                    d	          rdS dS )
a  Return True when a cron final response should suppress delivery.

    Recognizes the bracketed ``[SILENT]`` sentinel (whole-response, first line,
    or last line) plus the bracketless ``SILENT`` / ``NO_REPLY`` / ``NO REPLY``
    variants the model emits when it drops the brackets (#51438, #46917).
    Whitespace-trimmed and case-insensitive.  A token buried mid-sentence is
    treated as real content and delivered.
    Fliner   c                     d                     |                                                                                                           t          v S )Nr   )joinr   uppersplit_CRON_SILENCE_TOKENS)r   s    r)   	_is_tokenz,_is_cron_silence_response.<locals>._is_token  s:    xx

**,,2244559MMMr+   Tc                 :    g | ]}|                                 |S r2   r   )rD   lns     r)   rF   z-_is_cron_silence_response.<locals>.<listcomp>  s%    >>>B288::>R>>>r+   r   r   )
isinstancer:   r   bool
splitlinesr   
startswith)r&   strippedr   linesr   s        r)   _is_cron_silence_responser     s     dC   uzz||H uN N N N N N y t ?>(--//>>>E ))E!H%% 59)=)= t NNE
## t5r+   _parallel_pool_parallel_pool_max_workers_running_job_ids_sequential_poolc                   :    e Zd ZdZd	dZd	dZd	dZd	dZd	dZdS )
_ReadWriteLocku6  Writer-preferring readers-writer lock.

    Guards the process-global ``os.environ["TERMINAL_CWD"]`` override that a
    workdir cron job applies for the whole of its agent run.  Workdir jobs are
    writers: they mutate the shared env and need exclusive access.  Workdir-less
    jobs are readers: they only observe ``TERMINAL_CWD`` (indirectly, via the
    terminal / file / code-exec tools), so any number of them may run
    concurrently with each other, but none may run alongside a writer — that is
    exactly what stops a workdir-less job from picking up another job's workdir
    override and running its commands in the wrong directory.

    Writer preference bounds the wait for a workdir job (dispatched on the
    single-thread sequential pool) so a stream of workdir-less readers cannot
    starve it.
    r   Nc                     t          j        t          j                              | _        d| _        d| _        d| _        d S )Nr   F)	threading	ConditionLock_cond_readers_writer_active_writers_waitingselfs    r)   __init__z_ReadWriteLock.__init__E  s9    ()9)9::
# !r+   c                     | j         5  | j        s| j        dk    r+| j                                          | j         | j        dk    +| xj        dz  c_        d d d            d S # 1 swxY w Y   d S )Nr      )r   r   r   waitr   r   s    r)   acquire_readz_ReadWriteLock.acquire_readK  s    Z 	 	% ")>)B)B
!!! % ")>)B)BMMQMM	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   AA##A'*A'c                     | j         5  | xj        dz  c_        | j        dk    r| j                                          d d d            d S # 1 swxY w Y   d S )Nr   r   )r   r   
notify_allr   s    r)   release_readz_ReadWriteLock.release_readQ  s    Z 	( 	(MMQMM}!!
%%'''	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(s   5A

AAc                 <   | j         5  | xj        dz  c_        	 | j        s| j        dk    r+| j                                          | j         | j        dk    +| xj        dz  c_        n# | xj        dz  c_        w xY wd| _        d d d            d S # 1 swxY w Y   d S )Nr   r   T)r   r   r   r   r   r   s    r)   acquire_writez_ReadWriteLock.acquire_writeW  s
   Z 	' 	'!!Q&!!+) &T]Q->->JOO%%% ) &T]Q->-> %%*%%%%%*%%%%%%"&D	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	's(   B=A(B(A::
BBBc                     | j         5  d| _        | j                                          d d d            d S # 1 swxY w Y   d S NF)r   r   r   r   s    r)   release_writez_ReadWriteLock.release_writea  s    Z 	$ 	$"'DJ!!###	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$s   !6::r   N)	r.   r/   r0   r1   r   r   r   r   r   r2   r+   r)   r   r   4  s          " " " "   ( ( ( (' ' ' '$ $ $ $ $ $r+   r   max_workersc                     t           t          | k    rFt           t                               dd           t          j                            | d          a | at           S )z0Return (or create) the persistent parallel pool.NFr   cancel_futureszcron-parallelr   thread_name_prefix)r   r   shutdown
concurrentfuturesThreadPoolExecutorr   s    r)   _get_parallel_poolr   l  se     !;{!J!J%##u#EEE#+>>#. ? 
 
 &1"r+   c                  `    t           !t          j                            dd          a t           S )a)  Return (or create) the persistent single-thread sequential pool.

    A single worker guarantees env-mutating jobs never overlap, even
    across ticks: a job queued by a newer tick waits for the previous tick's
    sequential jobs to finish rather than corrupting their os.environ
    state.
    Nr   zcron-seqr   )r   r   r   r   r2   r+   r)   _get_sequential_poolr   z  s7     %-@@) A 
 
 r+   c                      t            t                               dd           da dat           t                              dd           dadS dS )z/Shut down the persistent pools on process exit.NTFr   )r   r   r   r   r2   r+   r)   _shutdown_parallel_poolr     sa     !T%@@@%)"#!!tE!BBB $#r+   rV   c                 z    t          j                    rdS | #dt          |                                           v S dS )u  True when the Python interpreter is finalizing.

    A cron tick can fire while the gateway is tearing down — SIGTERM from
    ``hermes update`` / ``hermes gateway stop`` / systemd restart, or an
    OOM-kill. Once finalization starts, ``concurrent.futures`` refuses new
    work with ``RuntimeError: cannot schedule new futures after interpreter
    shutdown`` and asyncio's default executor is gone, so *any* attempt to
    schedule delivery (live-adapter, ``asyncio.run``, or a fresh pool) is
    doomed and only pollutes ``errors.log`` with a traceback. Callers use
    this to skip gracefully with a warning instead of crashing (#58720,
    #55924).

    ``exc`` lets a caller also treat an already-raised scheduling error as a
    shutdown signal: the ``concurrent.futures`` module-global flag can be set
    a hair before ``sys.is_finalizing()`` flips, so matching the error text is
    a safe fallback for that race.
    TNzcannot schedule new futuresF)sysis_finalizingr:   r   )rV   s    r)   _interpreter_shutting_downr     sA    $  t
 -C0@0@@@5r+   _hermes_homec                  ,    t           pt                      S )u  Resolve Hermes home dynamically while preserving test monkeypatch hooks.

    Cron is per-profile by design (#4707): the in-process ticker runs inside a
    profile-scoped gateway, so resolving the active HERMES_HOME at call time
    means a profile's jobs are stored AND executed under that profile's home
    (its .env, config.yaml, scripts, skills). Do not freeze this at import or
    anchor it at the shared default root — either re-breaks profile isolation.
    )r   r   r2   r+   r)   _get_hermes_homer     s     ,?,,,r+   c                  6    t                      } | dz  }||dz  fS )zHResolve cron lock paths at call time so profile/env changes are honored.rQ   z
.tick.lock)r   )hermes_homelock_dirs     r)   _get_lock_pathsr     s'    "$$KV#HX,,,r+   c                     |                      d          }t          |t                    sdS |                     d          }|                     d          }|r|r|S dS )up  Extract origin info from a job, preserving any extra routing metadata.

    Treats non-dict origins (free-form provenance strings, ints, lists from
    migration scripts or hand-edited jobs.json) as missing instead of
    crashing with ``AttributeError`` on ``origin.get(...)``. Without this
    guard, a job tagged with e.g. ``"combined-digest-replaces-x-and-y"``
    crashed every fire attempt with
    ``'str' object has no attribute 'get'`` — ``mark_job_run`` recorded the
    failure, but the next tick re-loaded the same poisoned origin and
    crashed identically until the field was patched manually (#18722).
    originNplatformchat_id)r   r   dict)r   r   r   r   s       r)   _resolve_originr     sg     WWXFfd## tzz*%%Hjj##G G 4r+   c                    |                      d          }t          |t                    r|S 	 |t                      pi }t          |                     di           pi                      dd                    S # t          $ r Y dS w xY w)u+  Whether a cron delivery should also be mirrored into the target chat's
    gateway session transcript.

    Default OFF — preserves the historical isolation guarantee (cron deliveries
    live only in the cron job's own session, never the target chat's history)
    byte-for-byte for everyone who does not opt in.

    Precedence (first decisive value wins):
      1. Per-job ``attach_to_session`` (bool) — set via the ``cronjob`` tool,
         lets one briefing job opt in without flipping global behaviour.
      2. Global ``cron.mirror_delivery`` (bool) in config.yaml.
      3. False.

    When enabled, the cron's final output is appended to the target session as
    an assistant turn via the existing ``gateway.mirror.mirror_to_session`` —
    the same primitive ``send_message`` uses — so the next user reply in that
    chat sees the brief in context (no "what is Task #2?" amnesia). This is
    alternation- and cache-safe: the append lands at a turn boundary between
    user turns, never mid-loop, and never mutates the cached system prompt.
    attach_to_sessionNrQ   mirror_deliveryF)r   r   r   r
   rS   )r   r3   r@   s      r)   _cron_mirror_delivery_enabledr     s    * gg)**G'4   ;--%2CSWWVR((.B334EuMMNNN   uus   A
A9 9
BBr   platform_namer   	thread_idc                    | sdS t          |                     dd                                                    t          |                                          k    rdS t          |                     dd                    t          |          k    rdS |                     d          }|$t          |          t          |pd          k    rdS dS )u  True when a delivery target is the job's own origin conversation.

    Mirroring is scoped to the origin session by design (see
    ``_maybe_mirror_cron_delivery``). A job created from a live gateway chat
    stamps that chat as ``origin`` (``cronjob_tools._origin_from_env``), and
    that session is guaranteed to exist — it is the very conversation the user
    was in when they scheduled the job. Fan-out targets (``deliver=all``,
    explicit ``platform:chat_id`` to some *other* chat, or a home-channel
    fallback for an origin-less API/script job) are deliberately NOT mirrored:
    they are broadcasts, not a continuation of a conversation, and may point at
    a chat the user never opened an agent session in.

    This makes the historical "cold-start" worry a non-case: when the mirror
    semantically applies (target == origin) the session always exists; when no
    session exists, the target was never the origin conversation, so we simply
    do not mirror.
    Fr   r   r   r   NT)r:   r   r   )r   r   r   r   origin_threads        r)   _target_matches_originr     s    &  u
6::j"%%&&,,..#m2D2D2J2J2L2LLLu
6::i$$%%W55u JJ{++M S%7%73yB;O;O%O%Ou4r+   F)enabledmirror_textuser_idr   c          	      L   |sdS |pd                                 }|sdS 	 ddlm}  ||t          |          d|                     d          p|                     dd           d	| d||d
          }	|	r3t
                              d|                     dd          ||           dS t
                              d|                     dd          ||           dS # t          $ r=}
t
                              d|                     dd          |||
           Y d}
~
dS d}
~
ww xY w)u  Best-effort mirror of a cron delivery into the origin chat's session.

    No-op unless ``enabled`` (resolved once by the caller, and already scoped to
    the origin target — see ``_target_matches_origin``). Reuses the shipped
    ``mirror_to_session`` so cron rides exactly the same path that interactive
    ``send_message`` mirroring already uses, including passing ``user_id`` so a
    per-user-isolated group chat resolves to the exact member who scheduled the
    job (parity with ``send_message``). All failures are swallowed — a delivery
    that succeeded must never be reported as failed because the transcript
    mirror hit a problem.

    Because the caller only enables this for the target that equals the job's
    origin conversation, the session is expected to exist (the job was born in
    that session). A missing session therefore indicates an origin-less /
    fan-out delivery that should not have been mirrored anyway, and is treated
    as a silent no-op — never a synthetic session is created.
    Nr   r   mirror_to_session[Cron delivery: r   r   rQ   ]
usersource_labelr   r   rolez9Job '%s': mirrored delivery into %s:%s session transcript?uX   Job '%s': delivery mirror skipped for %s:%s (no matching gateway session — cold start)z.Job '%s': delivery mirror failed for %s:%s: %s)	r   gateway.mirrorr   r:   r   rT   infodebugrS   )r   r   r   r   r   r   r   r&   r   okes              r)   _maybe_mirror_cron_deliveryr   (  s   6  2$$&&D #
444444 LLRswwvG#''$2G2GRRDRR
 
 
  
	KKKc""M7    
 LL?c""M7    
  
 
 
<GGD#w	
 	
 	
 	
 	
 	
 	
 	
 	

s   BC )1C 
D#&2DD#c           
         t          |dd          }t          |          r|dS |                     d          p|                     dd          }d| }	 ddlm}  |t          |          |          } |||          }	|	dS |	                    d	
          }
|
rt          |
          ndS # t          $ rK}t          	                    d|                     dd          t          |dd          |           Y d}~dS d}~ww xY w)u  Open a dedicated thread for a continuable cron job (thread-preferred).

    Returns the new ``thread_id`` on success, or ``None`` when the platform has
    no thread primitive (WhatsApp/Signal/SMS) or creation failed — the ``None``
    return is the caller's signal to fall back to the origin-DM mirror, the same
    open-thread-or-fallback shape as ``GatewayRunner._process_handoff``. Reuses
    the shipped ``adapter.create_handoff_thread``; no new adapter surface.
    create_handoff_threadNr   r   rQ   u   Hermes — r   safe_schedule_threadsafe   r   uV   Job '%s': create_handoff_thread failed on %s — falling back to DM-session mirror: %sr   )
getattrcallabler   agent.async_utilsr   r:   rK   rS   rT   r   )r   adapterr   loopcreate_thread	task_namethread_namer   corofuturenew_thread_idr   s               r)   _open_continuable_cron_threadr  n  s=    G%<dCCMM"" dlt83774#8#8I+	++K>>>>>>}S\\;77))$55>4b11%2<s=!!!<   $GGD# = =q	
 	
 	

 ttttts   -B. (B. .
D8A C>>D	chat_namec                 ,   |pd                                 }|sdS 	 ddlm} ddlm}	 t          |dd          }
|
{	  ||                                          }n# t          t          f$ r d}Y nw xY w|A |	|t          |          |ddd	t          |          
          }|

                    |           ddlm}  ||t          |          d|                     d          p|                     dd           d| dt          |          dd           t                              d|                     dd          |||           dS # t           $ r>}t                              d|                     dd          ||||           Y d}~dS d}~ww xY w)u:  Seed the freshly-opened cron thread's session with the brief.

    Without this the brief is *visible* in the new thread but absent from any
    transcript, so the user's first reply in-thread would hit a session with no
    record of it ("what is Task #2?"). We create the thread-keyed session (the
    same key the user's reply will resolve to — ``build_session_key`` keys
    threads as participant-shared, so no ``user_id`` is needed) and append the
    brief as an assistant turn via the shipped ``mirror_to_session``.

    Mirrors ``GatewayRunner._process_handoff``'s seed step, but standalone:
    cron reaches the live ``SessionStore`` through the adapter's
    ``_session_store`` handle rather than the gateway object. Best-effort — a
    delivery that already succeeded is never failed by a seeding problem.
    r   Nr   PlatformSessionSource_session_storethreadzsystem:cronCron)r   r   r  	chat_typer   	user_namer   r   r   r   r   rQ   r   r   r   zDJob '%s': opened continuable thread %s on %s:%s and seeded the briefr   z=Job '%s': seeding cron thread session failed for %s:%s:%s: %s)r   gateway.configr	  gateway.sessionr  r   r   
ValueErrorKeyErrorr:   get_or_create_sessionr   r   r   rT   r   rS   r   )r   r   r   r   r   r   r  r&   r	  r  session_storeplatform_enumdest_sourcer   r   s                  r)   _seed_cron_thread_sessionr    s/   . 2$$&&D 0
++++++111111)94@@$% ()<)<)>)> ? ?) % % % $%(+m*LL'&)$!)nn   33K@@@444444 	LLRswwvG#''$2G2GRRDRR)nn!	
 	
 	
 	
 	RGGD#	='	
 	
 	
 	
 	
  
 
 
KGGD#w	1	
 	
 	
 	
 	
 	
 	
 	
 	

s;   E A E A0-E /A00CE 
F3FFr  is_dmc          
      b   |pd                                 }|sdS 	 ddlm}	 ddlm}
 |rdnd}t          |dd	          }|~	  |	|                                          }n# t          t          f$ r d	}Y nw xY w|D |
|t          |          |||rt          |          nd	d	
          }|
                    |           ddlm}  ||t          |          d|                     d          p|                     dd           d| dd	|rt          |          nd	d          }|r2t                              d|                     dd          |||           t!          |          S # t"          $ r=}t                              d|                     dd          |||           Y d	}~dS d	}~ww xY w)u  Seed the FLAT (thread_id=None) session for an ``in_channel`` cron delivery.

    The ``in_channel`` surface (D1/D2) delivers the brief flat into the channel
    with no thread, so the continuation surface is the whole-channel /
    whole-DM session keyed ``thread_id=None`` — the same bucket
    ``reply_in_thread: false`` routes an inbound plain reply to.

    Unlike the thread path, the shipped delivery-mirror alone is NOT sufficient
    here: ``mirror_to_session`` only APPENDS to a session that already EXISTS
    (``_find_session_id`` → no-op when none matches), and a flat channel
    ``(…, None)`` row is only created when a human posts a top-level message the
    bot processes — a ``chat_postMessage`` cron delivery never goes through the
    inbound handler, so the row is usually absent and the mirror silently drops
    the brief (verified live: the brief never landed, the reply had no context).
    So we CREATE the flat session row first, exactly like
    ``_seed_cron_thread_session`` does for threads, then mirror into it.

    The session KEY must match what the user's later inbound reply resolves to
    (``build_session_key``):
    - **Channel** (``chat_type="group"``): key is
      ``…:group:<chat_id>:<user_id>`` — user-isolated — so the seed MUST carry
      the **origin's real ``user_id``** (the member who scheduled the job), NOT
      a synthetic ``system:cron`` id, or the reply keys to a different session.
    - **1:1 DM** (``chat_type="dm"``): the key is ``…:dm:<chat_id>`` and does
      NOT embed ``user_id``, so any ``user_id`` resolves to the same session.
    ``chat_type`` mirrors the inbound handler's own choice
    (``"dm" if is_dm else "group"``, ``adapter.py``), so the seeded key is
    byte-identical to the reply's key.

    Returns True if a seed row was created and the brief mirrored, else False
    (caller falls back to the plain mirror). Best-effort — a delivery that
    already succeeded is never failed by a seeding problem.
    r   Fr   r  r
  dmgroupr  N)r   r   r  r  r   r   r   r   r   r   rQ   r   r   r   z@Job '%s': seeded flat in_channel session on %s:%s (chat_type=%s)r   z9Job '%s': seeding in_channel session failed for %s:%s: %s)r   r  r	  r  r  r   r   r  r  r:   r  r   r   r   rT   r   r   rS   r   )r   r   r   r   r   r  r   r  r&   r	  r  r  r  r  r  r   r   r   s                     r)   _seed_cron_channel_sessionr    s@   X 2$$&&D u.++++++111111!.DDw	)94@@$% ()<)<)>)> ? ?) % % % $%(+m*LL'',3=CLLL"   33K@@@444444LLRswwvG#''$2G2GRRDRR$+5CLLL
 
 
  	KKRc""M7I   Bxx   GGGD#w	
 	
 	
 uuuuus<   %E' A  E'  A63E' 5A66C0E' '
F.12F))F.c           	         |                      d          }t          |t                    sdS g }dD ]}|                     |          }|t          |                              dd                              dd                                          }|r#|                    d| d	|dd
                    |rdd                    |          z   ndS )a  Return safe provenance details for security warnings about a cron job.

    The scheduler normally has no live HTTP request object when it detects a
    bad stored ``context_from`` reference. Including the job's saved origin
    makes future probe logs actionable without exposing secrets: platform/chat
    metadata for gateway-created jobs, and optional source-IP fields for API
    surfaces that persist them in origin metadata.
    r   r   )r   r   r   	source_ipremoteforwarded_forNr   
origin_=   )r   r   r   r:   replacer   r;   r   )r   r   fieldskeyvaluer&   s         r)   _cron_job_origin_log_suffixr-  @  s     WWXFfd## rF[ : :

3=5zz!!$,,44T3??EEGG 	:MM8C88$tt*88999%+33&!!!!3r+   c                     	 ddl m}  |             ddlm} |                    |                                           }|r|j        r|j        S n# t          $ r Y nw xY wdS )zReturn the cron home-channel env var registered by a plugin platform.

    Falls through the platform registry so plugins that set
    ``cron_deliver_env_var`` on their ``PlatformEntry`` get cron delivery
    support without editing this module.
    r   discover_pluginsplatform_registryr   )hermes_cli.pluginsr0  gateway.platform_registryr2  r   r   cron_deliver_env_varrS   )r   r0  r2  entrys       r)   _plugin_cron_env_varr7  X  s    777777??????!%%m&9&9&;&;<< 	.U/ 	.--   2s   AA 
AAc                 x    |                                  }|t          v rdS t          t          |                    S )zWhether ``platform_name`` is a valid cron delivery target.

    Hardcoded built-ins in ``_KNOWN_DELIVERY_PLATFORMS`` are checked first;
    plugin platforms registered via ``PlatformEntry`` are accepted if they
    provide a ``cron_deliver_env_var``.
    T)r   _KNOWN_DELIVERY_PLATFORMSr   r7  )r   r   s     r)   _is_known_delivery_platformr:  k  s=       D(((t$T**+++r+   c                     |                                  }t                              |          }|r|S t          |          S )zReturn the env var name for a platform's cron home channel.

    Built-in platforms are in ``_HOME_TARGET_ENV_VARS``; plugin platforms are
    resolved from the platform registry.
    )r   _HOME_TARGET_ENV_VARSr   r7  )r   r   env_vars      r)   _resolve_home_env_varr>  x  sC       D#''--G %%%r+   c                     t          |           }|sdS t          j        |d          }|s1t                              |          }|rt          j        |d          }|S )zGReturn the configured home target chat/room ID for a delivery platform.r   )r>  osgetenv_LEGACY_HOME_TARGET_ENV_VARSr   )r   r=  r,  legacys       r)   _get_home_target_chat_idrD    sf    #M22G rIgr""E *-11':: 	*Ifb))ELr+   c                    t          |           }|sdS |                                 dk    r+t          j        dd                                          }|r|S t          j        | dd                                          }|sFt
                              |          }|r*t          j        | dd                                          }|pdS )u?  Return the optional thread/topic ID for a platform home target.

    Telegram-only override: ``TELEGRAM_CRON_THREAD_ID`` takes precedence over
    ``TELEGRAM_HOME_CHANNEL_THREAD_ID`` for cron delivery. When topic mode is
    enabled, deliveries that land in the root DM (thread_id unset) end up in
    the system-only lobby where the user cannot reply — the gateway returns
    the lobby reminder and drops ``reply_to_message_id`` (#24409). Pointing
    cron at a dedicated topic via this env var lets replies work as expected
    without changing the lobby invariant.
    Nre   TELEGRAM_CRON_THREAD_IDr   
_THREAD_ID)r>  r   r@  rA  r   rB  r   )r   r=  cron_threadr,  rC  s        r)   _get_home_target_thread_idrI    s     $M22G t
**i 92>>DDFF 	I,,,b117799E A-11':: 	AI333R88>>@@E=Dr+   c               #      K   t           D ]} | V  	 ddlm}  |             ddlm} |                                D ] }|j        r|j        t           vr	|j        V  !dS # t          $ r Y dS w xY w)zIterate built-in + plugin platform names that expose a home channel.

    Used by the ``deliver=origin`` fallback when the job has no origin.
    r   r/  r1  N)	r<  r3  r0  r4  r2  plugin_entriesr5  r   rS   )r   r0  r2  r6  s       r)   _iter_home_target_platformsrL    s      
 &  



777777??????&5577 	! 	!E) !ej@U.U.Uj   	! 	!    s   AA 
A-,A-c            
          g } 	 ddl m}  |            }d |                                D             }n:# t          $ r- t                              dd           t                      }Y nw xY wt                      D ]}||vrt          |          st          |          }| 
                    ||                    dd                                          t          t          |                    |pd	d
           | S )u  Return the platforms a cron job can auto-deliver to.

    Single source of truth for any UI (dashboard dropdown, etc.) that lets a
    user pick a cron delivery target. A platform is included when it is a valid
    cron delivery platform AND its gateway is configured (enabled + credentials
    present). Each entry reports whether the platform's home target (the
    room/channel cron posts to) is set — a platform can be configured for
    interactive use but still lack the home target an unattended cron job needs.

    Returns a list of dicts: ``{"id", "name", "home_target_set", "home_env_var"}``
    ordered by the gateway's canonical platform order. Callers should always
    prepend the implicit ``local`` option themselves — it needs no config.
    r   )load_gateway_configc                     h | ]	}|j         
S r2   )r,  rD   ps     r)   	<setcomp>z(cron_delivery_targets.<locals>.<setcomp>  s    OOOQWOOOr+   z1cron_delivery_targets: gateway config unavailableTexc_info_r   N)r   r   home_target_sethome_env_var)r  rN  get_connected_platformsrS   rT   r   rI   rL  r:  r>  r;   r)  titler   rD  )targetsrN  gateway_config	connectedr   r=  s         r)   cron_delivery_targetsr]    s;    G666666,,..OOn&L&L&N&NOOO		   HSWXXXEE			 ,-- 
 
y  *400 	'--S#..4466#'(@(F(F#G#G '4	 	
 	
 	
 	
 Ns   .3 4A*)A*deliver_valuec                    t          |           }|dk    rdS |dk    r|r2|d         t          |d                   |                    d          dS t                      D ]l}t	          |          }|rYt
                              d|                     d	|                     d
d                    |           ||t          |          dc S mdS d|v r|                    dd          \  }}|	                                }ddl
m}  |||          \  }}	}
|
r||	}}n|d}}	 ddlm}  |||          }|r |||          \  }}	}|r|}|	|	}n|}n# t          $ r Y nw xY w|||dS |}|rE|                    d          |k    r,|t          |d                   |                    d          dS t          |          sdS t	          |          }|sdS ||t          |          dS )z9Resolve one concrete auto-delivery target for a cron job.localNr   r   r   r   )r   r   r   zJJob '%s' has deliver=origin but no origin; falling back to %s home channelr   r   r   :r   r   )_parse_target_ref)resolve_channel_name)r   r:   r   rL  rD  rT   r   rI  r   r   tools.send_message_toolrb  gateway.channel_directoryrc  rS   r:  )r   r^  r   r   r   restplatform_keyrb  parsed_chat_idparsed_thread_idis_explicitr   rc  resolvedresolved_is_explicits                  r)   _resolve_single_delivery_targetrm    s    S!!Ft   	":.vi011#ZZ44   9:: 	 	M.}==G 
`GGFCGGD#$6$677!   !.&!;M!J!J    
 t
m+11#q99t$**,,======8I8I,X\8]8]5(+ 	,!/1AYGG!%tYG	FFFFFF++L'BBH 'IZIZ[giqIrIrF 02F' ',G'3$4	&G 	 	 	D	 &"
 
 	
 "M 
&**Z((M99%6),--K00
 
 	
 '}55 t&}55G t "/>>  s   "/E 
EEc                     | | dk    rdS t          | t          t          f          r%d | D             }|rd                    |          ndS t	          |           S )u  Normalize a stored/submitted ``deliver`` value to its canonical string form.

    The contract is that ``deliver`` is a string (``"local"``, ``"origin"``,
    ``"telegram"``, ``"telegram:-1001:17"``, or comma-separated combinations).
    Historically some callers — MCP clients passing an array, direct edits of
    ``jobs.json``, or stale code paths — have stored a list/tuple like
    ``["telegram"]``.  ``str(["telegram"])`` would serialize to the literal
    string ``"['telegram']"``, which is not a known platform and fails
    resolution silently.  Flatten lists/tuples into a comma-separated string
    so both forms work.  Returns ``"local"`` for anything falsy.
    Nr   r`  c                     g | ]D}t          |                                          #t          |                                          ES r2   r:   r   rP  s     r)   rF   z,_normalize_deliver_value.<locals>.<listcomp>L  s9    CCCACFFLLNNCQCCCr+   ,)r   rR   tupler   r:   )deliverpartss     r)   _normalize_deliver_valueru  =  sd     'R--w'D%=)) 5CCCCC"'4sxxW4w<<r+   allpartc                     |                                  }|t          vr| gS g }t                      D ]&}t          |          r|                    |           '|S )a@  Expand a routing-intent token to concrete platform names.

    ``all`` expands to every platform in ``_iter_home_target_platforms()``
    that has a configured home chat_id right now.  Unknown / non-token
    values pass through unchanged as a single-element list, so the caller
    can treat every token uniformly.
    )r   _ROUTING_TOKENSrL  rD  r;   )rw  tokenexpandedr   s       r)   _expand_routing_tokensr|  X  se     JJLLEO##vH466 + +#M22 	+OOM***Or+   c                    t          |                     dd                    }|dk    rg S d |                    d          D             }g }|D ]$}|                    t	          |                     %t                      }g }|D ]}t          | |          }|rq|d                                         t          |d                   |                    d          f}	|	|vr*|	                    |	           |
                    |           |S )a  Resolve all concrete auto-delivery targets for a cron job.

    Accepts the legacy comma-separated ``deliver`` string plus the
    ``all`` routing-intent token, which expands to every platform with
    a configured home channel.  Tokens may be combined with explicit
    targets: ``origin,all`` and ``all,telegram:-100:17`` both work.
    Duplicate (platform, chat_id, thread_id) tuples are collapsed by the
    existing dedup pass.
    rs  r`  c                 ^    g | ]*}|                                 |                                 +S r2   r   rP  s     r)   rF   z-_resolve_delivery_targets.<locals>.<listcomp>x  s-    DDDq!''))DDDDr+   rq  r   r   r   )ru  r   r   extendr|  rI   rm  r   r:   addr;   )
r   rs  	raw_partsrt  rawseenrZ  rw  targetr+  s
             r)   _resolve_delivery_targetsr  j  s    'swwy''B'BCCG'	DDGMM#$6$6DDDI E 2 2+C00111155DG ' '0d;; 	'*%++--s6)3D/E/EvzzR]G^G^_C$v&&&Nr+   c                 8    t          |           }|r|d         ndS )zAResolve the concrete auto-delivery target for a cron job, if any.r   N)r  )r   rZ  s     r)   _resolve_delivery_targetr    s"    ',,G *71::d*r+   >   .3gp.avi.mkv.mov.mp4.webm>   .gif.jpg.png.jpeg.webpmedia_filesmetadatac                    ddl m} ddlm}m}	 |                    |          }|D ]\  }
}	  ||
          j                                        }||nt          | dd          } |	|||          r| 	                    ||
|          }n\|t          v r|                     ||
|          }n:|t          v r|                     ||
|	          }n|                     ||
|
          }ddlm}  |||          }|3t"                              d|                    dd          |
            dS 	 |                    d          }n## t*          $ r |                                  w xY w|rQt          |dd          s@t"                              d|                    dd          |
t          |dd                     # t.          $ r<}t"                              d|                    dd          |
|           Y d}~d}~ww xY wdS )u;  Send extracted MEDIA files as native platform attachments via a live adapter.

    Routes each file to the appropriate adapter method (send_voice, send_image_file,
    send_video, send_document) based on file extension — mirroring the routing logic
    in ``BasePlatformAdapter._process_message_background``.
    r   r   )BasePlatformAdaptershould_send_media_as_audioNr   )is_voice)r   
audio_pathr  )r   
video_pathr  )r   
image_pathr  )r   	file_pathr  r   z8Job '%s': cannot send media %s, gateway loop unavailabler   r   r   r   successTz&Job '%s': media send failed for %s: %sr   unknownz%Job '%s': failed to send media %s: %s)pathlibr   gateway.platforms.baser  r  filter_media_delivery_pathssuffixr   r   
send_voice_VIDEO_EXTS
send_video_IMAGE_EXTSsend_image_filesend_documentr   r   rT   rU   r   rK   TimeoutErrorcancelrS   )r   r   r  r  r   r   r   r   r  r  
media_path	_is_voiceextroute_platformr  r   r  rK   r   s                      r)   _send_media_via_adapterr    s    VVVVVVVV%AA+NNK!,  g  g
I	g$z"")//11C)1)=XX77T^`dCeCeN)).#	RRR g))'j[c)dd##))'j[c)dd##..w:`h.ii,,W
]e,ffBBBBBB--dD99F~NGGD#&&
   r22     gfi>> <GGD#&&
GFGY4W4W    	g 	g 	gNNBCGGDRUDVDVXbdeffffffff	g? g  gs1   C>F>/EF> E&&AF>>
H1G??Hc                 h    | dS t          | d          sdS t          t          | d                    S )u  Return True only if ``send_result`` unambiguously confirms delivery.

    A live adapter that returns ``None`` (e.g. a swallowed exception, a busy
    platform, or a code path that returns early without producing a
    ``SendResult``) must NOT be treated as success — doing so causes the
    scheduler to log ``"delivered to <chat> via live adapter"`` while the
    gateway never actually sees the message (#47056).

    Likewise, an object missing a ``success`` attribute (e.g. a bare ``dict``
    or a partial mock) is a contract violation: it does not actually tell us
    whether the send succeeded.  Require an explicit, truthy ``success``
    attribute to count as confirmed.
    NFr  )hasattrr   r   )send_results    r)   _confirm_adapter_deliveryr    s>     u;	** uY//000r+   runtime_adapterr   job_idc                 &   t          t          |           dd          }t          |          sdS 	 ddlm}  | || t          |                    |          }|dS |                    d          }n/# t          $ r" t          	                    d||d	
           Y dS w xY wt          |t                    o9t          |                    d          pd                                          dk    }|rt                              d||           |S )u  Decide whether an (already-ambiguous) Telegram topic target is a genuine
    Bot API *channel* Direct-Messages topic (route via
    ``direct_messages_topic_id``) rather than a forum-style topic in a private
    chat (route via ``message_thread_id``).

    Callers gate this on the ambiguous shape first
    (``telegram:<positive_chat_id>:<numeric_thread_id>``) — that shape is
    identical for both cases, so shape alone cannot decide (this was the #52060
    regression).  The real signal is the chat *type*: a genuine channel DM topic
    lives on a ``channel`` chat.  Probe the live adapter's ``get_chat_info`` once
    and only return True when the chat is a channel.

    Fails SAFE to ``message_thread_id`` (returns False) for adapters without a
    probe, or any probe error/timeout — that is the pre-#22773 behaviour and the
    correct default for the common forum-topic case.
    get_chat_infoNFr   r   
   r   u\   Job '%s': get_chat_info probe failed for chat=%s — defaulting to message_thread_id routingTrS  typer   channeluG   Job '%s': chat=%s is a channel — routing via direct_messages_topic_id)r   r  r   r   r   r:   rK   rS   rT   r   r   r   r   r   r   )	r  r   r   r  r  r   r  r   
is_channels	            r)   _is_channel_dm_topicr    sO   4 D11?DIIMM"" u>>>>>>))M/3w<<88$
 
 >5 }}R}((   6Gd 	 	
 	
 	

 uu D$''\C0@0@0FB,G,G,M,M,O,OS\,\J 
UG	
 	
 	
 s   +A5 A5 5(B! B!contentc                   K t          |           }|st          |                     dd                    }|dk    rdS |dk    rEt                              d|                     d|                     dd                               dS d	| }t                              d
| d         |           |S ddlm} ddlm	}m
}	 d}
d}	 t                      }|                    di                               dd          }
n# t          $ r Y nw xY w|
rB|                     d| d                   }|                     dd          }d| d| d| d| d	}n|}ddlm} |                    |          \  }}|                    |          }	 t#          | |          }n# t          $ r d}Y nw xY wd}|r.|                    |          \  }}|pd                                }	  |            }n@# t          $ r3}d| }t                              d
| d         |           |cY d}~S d}~ww xY wg }|D ]}|d         }|d         }|                    d          }t)          |           pi }|                    d          }|r;|s9t                              d| d         ||                     dd          |           n&|r$t                              d| d         |||           |ot-          ||||          }|r|                    d          nd}	  |	|                                          } nU# t0          t2          f$ rA d | d!}t                              d
| d         |           |                    |           Y Nw xY w|j                            |           }!|!r|!j        s?d"| d#}t                              d
| d         |           |                    |           |pi                     |           }"d}#g }$d$}%	 |!j        pi                     d%          }&|&9t=          |&                                                                          d&k    rd&}%n# t          $ r d$}%Y nw xY w|%d&k    }'|'rE|"Ct?          |"d'd          s2t                              d(|                     dd          |           d}'t=          |                    d)          pd                                          }(|(d*k    p$|( o!t=          |                               d+          })d}*d}+d},|r |'s|"||stC          | |"||          }-|-r|-}|-},|"H|E t?          |d,d-                       r*dd.l"m#}.m$}/m%}0m&}1 | |	j'        k    o3|duo/ |1t=          |                    o |0t=          |                    }2|2otQ          |"||| d                   }3|3r,d}4t=          |          | d         d/}5d0t=          |          i}6n,|t=          |          nd}4d1| d         i}5|4r|4|5d<   |rd|ind}6	 |                                }7d}8d}9|7rdd2l)m*}:  |.||          }; |/| t=          |          |4d3          }< |:|;+                    |<|7|5          |          }=|=d}8|$                    d4           n^d}>d}?	 |=,                    d56          }>n# tZ          $ r |=.                                }@|@rEd7| d8| d9}t                              d:| d         |           |$                    |           d}8d}?n'd}9d}?t                              d;| d         ||           Y n*t          $ r}A|$                    d<|A             d}A~Aww xY w|?rnt_          |>t`                    r9tc          |>                    d=d                    }B|>                    d>          }Cn te          |>          }Bt?          |>d>d          }C|Bst_          |>t`                    r|>                    d?d@          }DdA}En,|>&t?          |>d?d          }Dtg          |>          j4        }EndB}DdC}Ed7| d8| dD|E dE|D dF	}t                              d:| d         |           |$                    |           d}8ns|Crq|ro|C                    dG          rZ|C                    dH          p|}FdI|F dJ| d8| dK}t                              d
| d         |           |                    |           |8r|9s|rtk          |"|||6|| | L           nS|9rQ|rOtm          |           dM| d8| dN}t                              d
| d         |           |                    |           |8rt                              dO| d         ||           d}#|,r-|+s+to          | |"|||,||                    dP          Q           d}+|'r.|r,|+s*tq          | |"||||)||                    dP          R          }*ts          | ||||||o|+ o|* S           nt# t          $ rg}dT| d8| dU| Ktu          KfdV|$D                       s|$                    K           t                              d:| d         K           Y d}~nd}~ww xY w|#stw                      rWdW| d8| dX}t                              d
| d         |           |$                    |           |<                    |$            || |!||||Y          }G	 t{          j>        |G          }Hn# t~          $ r}I|G@                                 tw          |I          r[dW| d8| dX}t                              d
| d         |           |$                    |           |<                    |$           Y d}I~I	v	 t          jB        C                    dZ[          }J	 |JD                    tz          j>         || |!||||Y                    }=|=,                    d\6          }H|JE                    d]           n# |JE                    d]           w xY wn# t          $ r}tw          |          r_dW| d8| dX}t                              d
| d         |           |$                    |           |<                    |$           Y d}~Y d}I~I
dW| d8| dU| }t                              d
| d         |d^           |$<                    |g           |<                    |$           Y d}~Y d}I~I
d}~ww xY wY d}I~Inud}I~Iwt          $ re}dW| d8| dU| }t                              d
| d         |d^           |$<                    |g           |<                    |$           Y d}~fd}~ww xY w|Hro|H                    d?          rZd_|Hd?          }t                              d
| d         |           |$<                    |g           |<                    |$           t                              d`| d         ||           ts          | ||||||o|+ S           |rdaF                    |          S dS )bu  
    Deliver job output to the configured target(s) (origin chat, specific platform, etc.).

    When ``adapters`` and ``loop`` are provided (gateway is running), tries to
    use the live adapter first — this supports E2EE rooms (e.g. Matrix) where
    the standalone HTTP path cannot encrypt.  Falls back to standalone send if
    the adapter path fails or is unavailable.

    Returns None on success, or an error string on failure.
    rs  r`  Nr   uk   Job '%s': deliver=origin but no origin or home channels — skipping delivery (output saved in last_output)r   r   r   z(no delivery target resolved for deliver=Job '%s': %sr   )_send_to_platform)rN  r	  TrQ   wrap_responser   zCronjob Response: z

(job_id: z)
-------------

zI

To stop or manage this job, send me a new message (e.g. "stop reminder z").)r  Fzfailed to load gateway config: r   r   r   zUJob '%s': origin has thread_id=%s but delivery target lost it (deliver=%s, target=%s)z*Job '%s': delivering to %s:%s thread_id=%sr   zunknown platform ''z
platform 'z' not configured/enabledr  cron_continuable_surface
in_channelsupports_inchannel_continuablezOJob '%s': cron_continuable_surface=in_channel not supported on %s, using threadr  r  D
is_runningc                      dS r   r2   r2   r+   r)   <lambda>z!_deliver_result.<locals>.<lambda>  s    di r+   )DeliveryRouterDeliveryTarget_looks_like_int#looks_like_telegram_private_chat_id)direct_messages_topic_idr  r  r  r   )r   r   r   rj  z)live adapter event loop scheduling failed<   r   zlive adapter send to ra  z. timed out before the coroutine was dispatchedz(Job '%s': %s, falling back to standalonezJob '%s': live adapter send to %s:%s timed out after 60s; already dispatched (in flight), assuming delivered (skipping standalone fallback to avoid duplicate)zlive adapter send failed: r  raw_responser   r  r   zno response from adapterNonez returned unconfirmed result (z, error=)thread_fallbackrequested_thread_idzconfigured thread_id z for z+ was not found; delivered without thread_id)r   z& media attachment(s) not delivered to z& (live adapter confirmation timed out)z-Job '%s': delivered to %s:%s via live adapterr  r  )r  r   r  )r   r   r   zlive adapter delivery to z	 failed: c              3       K   | ]}|v V  	d S Nr2   )rD   errerr_msgs     r)   	<genexpr>z"_deliver_result.<locals>.<genexpr>  s'      CCc7c>CCCCCCr+   zdelivery to u)    skipped — interpreter is shutting down)r   r  r   r   r   )r   rS  zdelivery error: zJob '%s': delivered to %s:%s; )Gr  ru  r   rT   r   rU   rd  r  r  rN  r	  r
   rS   r  r  extract_mediar  r   r   r   r   r   r   r   r  r  r;   	platformsr   extrar:   r   r   r  gateway.deliveryr  r  r  r  TELEGRAMr  r   r   _deliver_to_platformrK   r  r  r   r   r   r  r  r.   r  r#   r  r  r   anyr   r  asynciorunRuntimeErrorcloser   r   r   submitr   r   )Lr   r  adaptersr   rZ  r^  msgr  rN  r	  r  user_cfgr   r  delivery_contentr  r  cleaned_delivery_contentmirror_enabledr   rU  configr   delivery_errorsr  r   r   r   r   r   mirror_this_targetorigin_user_idr   pconfigr  	deliveredtarget_errorssurface_modesurface_rawin_channel_surfaceorigin_chat_typeis_dm_targetinchannel_seededthread_seededopened_thread_idr  r  r  r  r  is_ambiguous_telegram_topicroute_via_dm_topicroute_thread_idroute_metadatamedia_metadatatext_to_send
adapter_ok	timed_outr   routerroute_targetr  r  timeout_handled	cancelledexsend_successsend_raw_responser  shaper  r  rK   run_errpoolr  sL                                                                              @r)   _deliver_resultr    s    (,,G 0G1L1LMMG##4 H$$KKBc 2 233  
 4HHH~s4y#666
999999<<<<<<<<
 MH== VR0044_dKK     #GGFCI..	r""g g gg g g g Xa	g g g 	 # ;:::::,?,M,MN^,_,_)K)%AA+NNK6sHEE   K 2,::7CC;"(b//11$$&&   333^SY444






 O ] ]z*#JJ{++	 !%%+

;// 
	 
	NN*D	=#'')W*E*Ev   
  	LL<D	='9   , 
0FM7I1
 1

 3ENI...$	x 3 3 5 566HHH% 	 	 	7}777CNN>3t9c:::""3'''H		 "&&x00 	go 	F}FFFCNN>3t9c:::""3''' $>r..x88	  	$"=.B334NOOK&3{+;+;+A+A+C+C+I+I+K+K|+[+[+ 	$ 	$ 	$#LLL	$)\9 		'/"=g=uG
 G
"= LL#c""M  
 "' vzz+66<"==CCEE'4/ 
  AS\\%<%<S%A%A 	 !& *.	1&	1  +  ! :_gt M  1 *	#0 &4+;@jl\i\i@j@j@l@l+;            H-- 4T)477GEE4 $OC	NN33	 ( "= "AU$D	B B " Q #'03I!$i" " #=c)nn!M 5>4I#i...t"*CI!6" B2AN;/=F!P+y!9!9DO  8==??!
!	 D<JJJJJJ+^FH==F#1>!) #G"1$(	$ $ $L 6533((* 
  F ~%*
%,,-XYYYY&**/0"*0---*C*CKK+ (" (" ("$ )/I( "%TM %T %TG %T %T %T !$ !'$N$'Is!" !" !" !. 4 4S 9 9 9-2
26,0	26 &%: %(I}g!" !" !"  ) " " " *001Rb1R1RSSS!" + 5< !  *+t<< _/3KOOIu4U4U/V/V4?OON4S4S 1 1/H/U/U4;KY]4^4^ 1#/ <#-k4#@#@ !3*5//'9*M*MC,2EE%0%<*1+w*M*MC,0,=,=,FEE*DC,2E%ZM %Z %ZG %Z %ZDI%Z %ZSV%Z %Z %Z !$ !'$N$'Is!" !" !" !. 4 4S 9 9 9-2

 1<$-< %6$9$9:K$L$L<
 7H6K6KLa6b6b6ofo 3%l<O %l %l'4%l %l7>%l %l %l !$ !'~s4y# N N N / 6 6s ; ; ;  0i 0K 0+'#&!)      0; 0{++ [ [([ [+2[ [ [  NN>3t9cBBB#**3/// KK OQTUYQZ\ikrsss $I ( - -1-,k&,jj&=&=   
 )-
 * .@  +E-'|$2&,jj&=&=	, , ,( 0]G["+^ 2 a=7H aQaMa   
    [m[[g[[XY[[CCCC]CCCCC 2!((111>Iw       	  P	 *++ g]ggWggg~s4y#>>>$$S)))&&}555$$XwAYen  }H  I  I  ID3 T** + + +
 

 .g66 kkkkkkCNN>3t9cBBB!((---#**=999HHHH%-@@Q@OOD2!%W[:K:KHV]_f  iA  MV  do  ;p  ;p  ;p  "q  "q!'r!:!:51111511111     2!44 !o]ooWooo~s4y#FFF%,,S111'..}=== NNNNN1NNCLLTC$LOOO!((#///#**=999HHHHHHHH 21111    J]JJWJJqJJ^SYdKKK$$cU+++&&}555  &**W-- :::^SY<<<$$cU+++&&}555KK6D	=RYZZZ']G[#^*@=/@     *yy)))4s+  8C> >
D
DF F('F(
G) )
H&3(H!H&!H&L**AM<;M<?AQQ&%Q& B	h*[ hB]6h	]6]11]66Jh
j Ajjlv%A8t&$ p=Ap!
p=!p99p=<t&=
tA$t+t&5Att&tt&&v3Avvi  c                  2   t           t          k    r[	 t          t          t                               } | dk    r| S n0# t          $ r# t
                              dt                      Y nw xY wt          j        dd          	                                }|rQ	 t          t          |                    } | dk    r| S n+# t          $ r t
                              d|           Y nw xY w	 t                      pi }t          |t                    r|                    di           ni }|                    d          }|$t          t          |                    } | dk    r| S n2# t          $ r%}t
                              d	|           Y d}~nd}~ww xY wt          S )
zOResolve cron pre-run script timeout from module/env/config with a safe default.r   z<Invalid patched _SCRIPT_TIMEOUT=%r; using env/config/defaultHERMES_CRON_SCRIPT_TIMEOUTr   z;Invalid HERMES_CRON_SCRIPT_TIMEOUT=%r; using config/defaultrQ   script_timeout_secondsNz2Failed to load cron script timeout from config: %s)_SCRIPT_TIMEOUT_DEFAULT_SCRIPT_TIMEOUTintfloatrS   rT   rU   r@  rA  r   r
   r   r   r   r   )r   	env_valuer3   cron_cfg
configuredrV   s         r)   _get_script_timeoutr  \  s   111	l%0011G{{  	l 	l 	lNNY[jkkkkk	l 	6;;AACCI e	e%	**++G{{  	e 	e 	eNNXZcddddd	e	Pmm!r*4S$*?*?G37762&&&R\\":;;
!%
++,,G{{ P P PI3OOOOOOOOP #"s:   (< *A)(A)#B; ;%C#"C#'A7E   
F*F

Fscript_pathc                    t                      dz  }|                    dd           |                                }t          |                                           }|                                r|                                }n||z                                  }	 |                    |           n# t          $ r dd| d| fcY S w xY w|                                sdd| fS |	                                sdd| fS t                      }|j                                        }|d	v rXt          j        d
          p"t          j                            d          rdnd}|dd|j        dfS |t'          |          g}nt(          j        t'          |          g}	 ddlm}	 t(          j        dk    rdt3                      ini }
t5          j        |fdd|t'          |j                   |	t          j                                                  d|
}|j        pd                                 }|j!        pd                                 }	 ddl"m#}  ||          } ||          }n6# tH          $ r)}tJ          &                    d|           d}d}Y d}~nd}~ww xY w|j'        dk    rVd|j'         g}|r|(                    d|            |r|(                    d|            dd)                    |          fS d|fS # t4          j*        $ r dd| d| fcY S tH          $ r}dd| fcY d}~S d}~ww xY w)uE  Execute a cron job's data-collection script and capture its output.

    Scripts must reside within HERMES_HOME/scripts/.  Both relative and
    absolute paths are resolved and validated against this directory to
    prevent arbitrary script execution via path traversal or absolute
    path injection.

    Supported interpreters (chosen by file extension):

    * ``.sh`` / ``.bash`` — run with ``/bin/bash``
    * anything else — run with the current Python interpreter
      (``sys.executable``), preserving the original behaviour for
      Python-based pre-check and data-collection scripts.

    Shell support lets ``no_agent=True`` jobs ship classic bash watchdogs
    (the `memory-watchdog.sh` pattern) without wrapping them in Python.

    Subprocess environment is passed through ``_sanitize_subprocess_env`` so
    provider credentials and other Hermes-managed secrets are not inherited
    (SECURITY.md §2.3), matching terminal and MCP child processes.

    Args:
        script_path: Path to the script.  Relative paths are resolved
            against HERMES_HOME/scripts/.  Absolute and ~-prefixed paths
            are also validated to ensure they stay within the scripts dir.

    Returns:
        (success, output) — on failure *output* contains the error message so the
        LLM can report the problem to the user.
    scriptsTparentsexist_okFz=Blocked: script path resolves outside the scripts directory (z): zScript not found: zScript path is not a file: >   .sh.bashbashz	/bin/bashNzCannot run .sh/.bash script z{: bash not found on PATH. On Windows, install Git for Windows (which ships Git Bash) or rewrite the script as Python (.py).r   )_sanitize_subprocess_envwin32creationflags)capture_outputr&   r   cwdenvr   )redact_sensitive_textz/Failed to redact sensitive text from output: %sz[REDACTED - redaction failed]zScript exited with code zstderr:
zstdout:
r%  zScript timed out after zs: zScript execution failed: )+r   mkdirresolver   
expanduseris_absoluterelative_tor  existsis_filer  r  r   shutilwhichr@  pathisfiler   r:   r   
executabletools.environments.localr#  r   r	   
subprocessr  parentenvironcopystdoutr   stderragent.redactr)  rS   rT   rU   
returncoder;   r   TimeoutExpired)r  scripts_dirscripts_dir_resolvedr  r3  script_timeoutr  _bashargvr#  popen_kwargsrK   r;  r<  r)  r   rt  rV   s                     r)   _run_job_scriptrF  }  sD   > #$$y0KdT222&..00
{


&
&
(
(C
 -{{}}c!**,,
-.... 
 
 
9$9 9)49 9
 	
 	
 	

 ;;== 2141111<<>> ;:D::::(**N [  F!!! V$$ 
7>>+66@KKD 	 =9ty 9 9 9 
 s4yy!D		*'8EEEEEEBE,RYBYBY);)=)=>>_a
"DK  (():):;;
 
 
 
 -%2,,..-%2,,..	5::::::**622F**622FF 	5 	5 	5NNLaPPP4F4FFFFFF	5
 !!C0ACCDE 3111222 3111222$))E****V|$ J J JIII4IIIIII 8 8 87#7777777778sh   B5 5CC$B-L
 I/ .L
 /
J"9JL
 J""A#L
 L
 
L?%	L?.L:4L?:L?script_outputc                 F   | sdS d |                                  D             }|sdS |d                                         }	 t          j        |          }n# t          j        t
          f$ r Y dS w xY wt          |t                    sdS |                    dd          duS )u  Parse the last non-empty stdout line of a cron job's pre-check script
    as a wake gate.

    The convention (ported from nanoclaw #1232): if the last stdout line is
    JSON like ``{"wakeAgent": false}``, the agent is skipped entirely — no
    LLM run, no delivery. Any other output (non-JSON, missing flag, gate
    absent, or ``wakeAgent: true``) means wake the agent normally.

    Returns True if the agent should wake, False to skip.
    Tc                 :    g | ]}|                                 |S r2   r   )rD   r   s     r)   rF   z$_parse_wake_gate.<locals>.<listcomp>  s%    RRRtTZZ\\RdRRRr+   r   	wakeAgentF)	r   r   jsonloadsJSONDecodeErrorr  r   r   r   )rG  stripped_lines	last_linegates       r)   _parse_wake_gaterQ    s      tRR}'?'?'A'ARRRN tr"((**Iz)$$ *-   ttdD!! t88K&&e33s   A A10A1prerun_scriptc                 z   t          |                     d          pd          }|}|                     d          }d}|                     d          }|r5||\  }}nt          |          \  }}|r|rd| d| }d	}ndS d
| d| }d	}|                     d          }	|	rsddlm}
 t          |	t                     r|	g}	|	D ]Q}|rt          d |D                       sRt                              d||                     d          |                     d          t          |                      p	 |
|z  }|
                                st          |                    d          d d	          }|s|d                             d                                          }d}t          |          |k    r|d|         dz   }|rd| d| d| }d	}n# t           t"          f$ r'}t                              d||           Y d}~Kd}~ww xY wd}||z   }||                     d          }|r|gng }nt          |t                     r|g}d |D             }|st%          || d||          S dd lm} dd!lm} dd"lm}m} g }g }|D ]} ||                    d#                    }|r ||dt          |                     d          pd          pd$          }|r3|\  }}}|r|                    d           |                    |           t                              d%|                     d|                     d                    |           |                    |           	 t9          j         ||                    } nu# t8          j        t>          f$ r\ t                              d&|                     d|                     d                    |           |                    |           Y sw xY w|                     d'          su|                     d(          pd)| d*}!t                              d+|                     d|                     d                    |!           |                    |           	  ||           n-# t@          $ r  t          !                    d,|d	-           Y nw xY wt          |                     d.          pd                                          }"|r|                    d           |"                    d/| d0d|"g           |rEd1d2#                    |           d3d2#                    |           d4}#|$                    d|#           |r|"                    dd5| g           t%          d6#                    |          | d	7          S )8a  Build the effective prompt for a cron job, optionally loading one or more skills first.

    Args:
        job: The cron job dict.
        prerun_script: Optional ``(success, stdout)`` from a script that has
            already been executed by the caller (e.g. for a wake-gate check).
            When provided, the script is not re-executed and the cached
            result is used for prompt injection. When omitted, the script
            (if any) runs inline as before.
    promptr   skillsFscriptNzq## Script Output
The following data was collected by a pre-run script. Use it as context for your analysis.

```
z
```

TzQ## Script Error
The data-collection script failed. Report this to the user.

```
context_fromr   )
OUTPUT_DIRc              3      K   | ]}|d v V  	dS )0123456789abcdefNr2   )rD   cs     r)   r  z$_build_job_prompt.<locals>.<genexpr>M  s(      +[+[A1C,C+[+[+[+[+[+[r+   z@context_from: skipping invalid job_id %r for job_id=%r name=%r%sr   r   z*.mdc                 4    |                                  j        S r  )statst_mtime)fs    r)   r  z#_build_job_prompt.<locals>.<lambda>\  s    !&&(("3 r+   )r+  reverseutf-8encodingi@  z

[... output truncated ...]z## Output from job 'zo'
The following is the most recent output from a preceding cron job. Use it as context for your analysis.

```
z2context_from: failed to read output for job %r: %su  [IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user — do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly "[SILENT]" (nothing else) to suppress delivery. Never combine [SILENT] with content — either report your findings normally, or say [SILENT] and nothing more.]

skillc                     g | ]D}t          |                                          #t          |                                          ES r2   rp  )rD   r   s     r)   rF   z%_build_job_prompt.<locals>.<listcomp>  s=    MMM3t99??;L;LM3t99??$$MMMr+   
has_skillshas_injected_datauser_prompt)
skill_view)bump_use)build_bundle_invocation_messageresolve_bundle_command_key/)user_instructiontask_idz>Cron job '%s': bundle '%s' could not load any skills, skippingz9Cron job '%s': skill '%s' returned invalid JSON, skippingr  r   zFailed to load skill 'r  u/   Cron job '%s': skill not found, skipping — %sz-Cron job: failed to bump skill usage for '%s'rS  r  z&[IMPORTANT: The user has invoked the "zf" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]ze[IMPORTANT: The following skill(s) were listed for this job but could not be found and were skipped: z, un   . Start your response with a brief notice so the user is aware, e.g.: '⚠️ Skill(s) not found and skipped: z']zPThe user has provided the following instruction alongside the skill invocation: r%  )rg  )%r:   r   rF  	cron.jobsrX  r   rv  rT   rU   r-  r/  rJ   glob	read_textr   r#   OSErrorPermissionError_scan_assembled_cron_prompttools.skills_toolrj  tools.skill_usagerk  agent.skill_bundlesrl  rm  lstripr;   rK  rL  rM  	TypeErrorrS   r   r  r   insert)$r   rR  ri  rT  rU  rh  r  r  rG  rW  rX  source_job_idjob_output_diroutput_fileslatest_output_MAX_CONTEXT_CHARSr   	cron_hintrC  skill_namesrj  rk  rl  rm  rt  skipped
skill_name
bundle_keybundle_payloadbundle_message_loaded_bundle_skills_missing_bundle_skillsloadedr   r  notices$                                       r)   _build_job_promptr    s    cggh''-2..KFWWXF  ''(##K %$%2"G]]%4[%A%A"G] 	%   *    	     %)!! t%     !% 77>**L +g((((((lC(( 	*(>L) '	g '	gM  +[+[]+[+[+[([([ V!GGDMMGGFOO/44   g!+m!;%,,.. %"''//33      
 $  ,Q 9 97 9 K K Q Q S S%)"}%%(:::$12E3E2E$FIi$iM  
$} $ $ !.$ $ "	$ $  )-%% & _- g g gSUbdeffffffffg	D  F~!!#+&	FC	 	  MMMMMK 
*/#
 
 
 	
 -,,,,,******________EG! 5
 5


 0/
0A0A#0F0FGG
 	<<!#CGGDMM/R008D  N
  P^M 57M %LL$$$^,,,NNP..  
 NN:&&&	Z

: 6 677FF$i0 	 	 	NNVX[X_X_`fhkhohopthuhuXvXv  yC  D  D  DNN:&&&H	 zz)$$ 	JJw''Q+QJ+Q+Q+QENNLcggV\^a^e^efj^k^kNlNlnstttNN:&&&	eHZ     	e 	e 	eLLH*_cLddddd	e fjj++1r2288:: 	LL l  l  l  l	
 	
 	
 	
   N!%7!3!3N N 8<yy7I7IN N N 	 	Q xbumsuuvwww&tyy'7'7NNNNsJ   3G(G6A GH+HH3NA.PPR'SSrf  	assembledrg  rh  ri  c                *   ddl m}m} |s|r" ||           \  }}|} |s|s|r ||          }n ||           }|rW|                    d          p|                    d          pd}	t                              d|	|           t          |          | S )u  Scan the fully-assembled cron prompt for injection patterns. Raises
    ``CronPromptInjectionBlocked`` when a match fires so ``run_job`` can
    surface a clear refusal to the operator.

    Plugs the #3968 gap: ``_scan_cron_prompt`` runs on the user-supplied
    prompt at create/update, but skill content is loaded from disk at
    runtime and was never scanned. Since cron runs non-interactively
    (auto-approves tool calls), a malicious skill carrying an injection
    payload bypassed every gate.

    Two pattern tiers, selected by what the assembled prompt CONTAINS,
    not just whether skills are attached:

    - When the assembled prompt is essentially the user prompt + the cron
      hint (no skills, no injected data), the STRICT ``_scan_cron_prompt``
      patterns apply: a bare ``rm -rf /`` in a small directive prompt is a
      smoking gun, not prose.
    - When the assembled prompt includes runtime-loaded content — skill
      markdown (``has_skills=True``) or DATA injected from a job script's
      stdout / an upstream job's output (``has_injected_data=True``) — the
      LOOSER ``_scan_cron_skill_assembled`` pattern set is used: only
      unambiguous prompt-injection directives block; command-shape
      patterns are dropped and invisible unicode is sanitized (stripped +
      logged) rather than blocked, to avoid false-positives that
      permanently kill a job. Skill bodies are vetted at install time by
      ``skills_guard.py``; script output is produced by operator-authored
      code, the same trust class — and data feeds (e.g. a triage bot
      ingesting bug reports) legitimately quote dangerous commands.

    When the looser tier is selected because of injected data only,
    ``user_prompt`` (the raw, pre-assembly prompt) is additionally scanned
    with the STRICT set so the user-authored surface keeps the full
    create/update-time guarantee at runtime (defense-in-depth for legacy
    jobs that predate the create-time scanner).
    r   )_scan_cron_prompt_scan_cron_skill_assembledr   r   z	<unknown>uC   Cron job '%s': assembled prompt blocked by injection scanner — %s)tools.cronjob_toolsr  r  r   rT   rU   r-   )
r  r   rg  rh  ri  r  r  r(   
scan_error	job_labels
             r)   rv  rv    s    V RQQQQQQQ 2& 2 98CC	 	8* 	8 	8 +*;77J&&y11
 5GGFOOCswwt}}C	Q	
 	
 	

 )444r+   c                    	 ddl m}  ||                     d          |                     d                    }nB# t          $ r5}|                     d          rd|j        j         d| d}nd}Y d}~nd}~ww xY w|rF|                     d	          }t                              d
||           t          d| d|           dS )u4  Fail closed if a job's stored provider/base_url pair would exfiltrate a
    credential (F8 runtime backstop; CWE-200/CWE-522).

    The model-callable cron tool validates this on create/update, but a job
    persisted before that guard — or written directly to the jobs store —
    reaches the scheduler's provider-resolution sink unchecked. Re-validate the
    EFFECTIVE stored pair with the same guard the tool uses, so a named
    provider's stored key is never paired with an off-host base_url at fire
    time. Raises ``RuntimeError`` (caught by the run_job failure path → the run
    is aborted and reported) when the pair is unsafe; returns ``None`` otherwise.

    Fallback providers come from operator config, not the model-callable job, so
    they are trusted and validated by the caller, not here.
    r   )_validate_cron_base_urlproviderbase_urlz+could not validate provider/base_url pair (: z=); refusing to run a job with an unverified base_url overrideNr   ud   Job '%s': refusing to run — unsafe provider/base_url pair could exfiltrate a stored credential: %s
Cron job 'z' blocked for safety: )	r  r  r   rS   	__class__r.   rT   r   r  )r   r  r  rV   r  s        r)   _guard_job_credential_exfilr  $	  s   ??????%%cggj&9&9377:;N;NOO    77: 	2M*2 2.12 2 2 C C#$  M1C	
 	
 	

 KKKcKKLLLM Ms   8; 
A:+A55A:defer_agent_teardownr  c                <   | d         }t          |                     d          p|                     d          p|pd          }|                     d          r|                     d          }|s$d}t                              d||           d	d
d
|fS |                     d          pd
                                pd}d}|r\t          |                                          r;t          j                    }	 t          j	        |           n# t          $ r d}Y nw xY w	 t          |          \  }}	|&	 t          j	        |           n># t          $ r Y n2w xY wn-# |&	 t          j	        |           w # t          $ r Y w w xY ww xY wt                                          d          }
|s d| d|	 d|
 }d| d| d|
 d|	 d	}d	|||	fS t          |	          s2t                              d|           d| d| d|
 d}d|t           dfS |	                                s2t                              d|           d| d| d|
 d}d|t           dfS d| d| d|
 d|	 d	}d||	dfS ddlm} d}	 ddlm}  |            }nG# t*          $ r:}t                              d|                     dd           |           Y d}~nd}~ww xY wd}|                     d          }|rwt          |          }|\  }}|rat          |          sRt                              d!||           d| d| dt                                          d           d"}d|t           dfS 	 t/          | |#          }nu# t0          $ rh}t                              d$|||           d| d| dt                                          d           d%| d&	}d	|d
t          |          fcY d}~S d}~ww xY w|&t                              d'|           dd
t           dfS t5          |           }d(| d)t                                          d*           }t                              d+||           t                              d,|dd-                    d}d.t          j        d/<   dd0lm}m}m}  |d
d
d
1          }d2}|D ]} ||                               d
           |                     d          pd
                                pd}|r?t          |                                          st                              d3||           d}t          j                            d4d5          }!|du}"|"rtB          "                                 ntB          #                                 	 |r+|t          j        d4<   t                              d6||           dd7l$m%}#m&}$  |$              |#tO                      8           tQ          |           }%|%r|d9                              |%d:                    |d;                              t          |%d<                              |d=                              |%                    d>          d
nt          |%d>                              |                     d?          pt          j)        d@          pd
}&i }'	 ddl*}(t          tO                      dAz            })t          j+        ,                    |)          rt[          |)dBC          5 }*|(.                    |*          pi }'ddd           n# 1 swxY w Y   	 ddDl/m0}+ |+1                    |'          }'n# t*          $ r Y nw xY wte          |'          }'|'                    d?          pi },|                     d?          s[tg          |,t                     r|,}&nCtg          |,th                    r.|,                    dE          p|,                    d?          }-|-r|-}&n3# t*          $ r&}t                              dF||           Y d}~nd}~ww xY wtg          |&t                     r|&                                sBtk          dG| dH|                     d?          dIt          j)        d@d
          dJ| dK	          	 ddLl6m7}. |'                    dMi           }/tg          |/th                    r!|/                    dN          r |.dO           n# t*          $ r Y nw xY wddPl6m8}0  |0|'                    dQi                               dRd
                    }1d}2tg          |'                    dQi           th                    r|'                    dQi           ni }3t          j)        dSd
          p+|'                    dTd
          p|3                    dTd
          }4|4rt          |4          9                                }5|5:                                stO                      |5z  }5|5,                                r	 t[          |5dUdBC          5 }6tw          j<        |6          }2ddd           n# 1 swxY w Y   tg          |2tz                    sd}2n6# t*          $ r)}t                              dV||5|           d}2Y d}~nd}~ww xY w|'                    dQi                               dW          p|'                    dW          pdX}7|'                    dY          pi }8ddZl>m?}9m@}: dd[lAmB}; t          |            	 d\|                     d]          i}<|                     d^          r|                     d^          |<d_<    |9di |<}=n_# |;$ r.}>t                              d`||>           t          |'          }?d}=|?D ]}@	 d\|@                    d]          i}A|@                    d^          r|@d^         |Ad_<   |@                    da          r|@da         |Adb<    |9di |A}=t                              dc||=                    d]                      nG# t*          $ r:}Bt                              dd||@                    d]          |B           Y d}B~Bd}B~Bww xY w|=tk           |:|>                    |>Y d}>~>n0d}>~>wt*          $ r }C |:|C          }Dtk          |D          |Cd}C~Cww xY wg }E|                     de          pd
                                E                                }F|Fr|                     d]          pd
                                slt          |=                    d]          pd
                                          E                                }G|Gr"|G|Fk    r|EF                    df|F dg|G dh           |                     di          pd
                                E                                }H|Hr|                     d?          pd
                                sYt          |&pd
                                          E                                }I|Ir"|I|Hk    r|EF                    dj|H dg|I dh           |ErHdkG                    |E          }Jt                              dl||J|           tk          dm|J dn| do          t          |'          pd}Kd}Lt          |=                    d]          pd
                                          E                                }M|Mr	 ddplHmI}N  |N|M          }O|OJ                                r>|O}Lt                              dq||Mt          |OL                                                     n4# t*          $ r'}t                              dr||M|           Y d}~nd}~ww xY w	 ddslMmN}P  |P            }Q|Qr)t                              dt|t          |Q                     n3# t*          $ r&}Rt                              du||R           Y d}R~Rnd}R~Rww xY w |di d?|&da|=                    da          d^|=                    d^          d]|=                    d]          dv|=                    dv          dw|=                    dx          dy|=                    dz          d{|7d||1d}|2d~|Kd|Ld|8                    d          d|8                    d          d|8                    d          d|8                    d          d|'                    d          pi                     d          dt          | |'          dt          |'          dddt          |           ddddd:dd|d|}t          j)        dd
                                          }S|SrE	 t          |S          }Tn6# t          t          f$ r  t                              d|S           d}TY nw xY wd}T|Tdk    r|Tnd}Ud}Vt          jV        W                    d          }Wt          jY                    }X|WZ                    |Xj[        |j\        |          }Yd	}Z	 |U|Y]                                }[nd}[	 t          jV        ^                    |Yh|V          \  }\}]|\r|Y]                                }[nXd}^t          |d          r<	 |`                                }_|_                    dd          }^n# t*          $ r Y nw xY w|^|Uk    rd}Znn&# t*          $ r |Wa                    d	d            w xY w|Wa                    d	d           n# |Wa                    d	d           w xY w|Zr"i }`t          |d          r&	 |`                                }`n# t*          $ r Y nw xY w|`                    dd          }a|`                    dd          }b|`                    d          }c|`                    dd          }d|`                    d{d          }et                              d||b|U|a|d|e|cpd           t          |d          r|b                    d           t          dG| dt          |b           dt          |U           d|a           tg          |[th                    s'tk          dt          |[          jf         d|[          t          |[                    d          pd
          }f|[                    d          pd
                                }g|[                    d          duo:|[                    d          d	u o#|fg                    d          ot          |g          }h|[                    d          du s|[                    d          d	u r*|hs(|[                    d          p|gpd}itk          |i          |hrt                              d|           |[                    dd
          pd
}j|j                                dk    rd
}j|j                                ru|frs	 |h                    |f          }kn# t*          $ r d
}kY nw xY w|krH|j                                |k                                k    rt                              d||f           d
}j|jr|jnd}ld| d| dt                                          d           d|                     dd           d| d|l d}	t                              d|           d|	|jdf|r6|!d5k    r!t          j        i                    d4d           n|!t          j        d4<   |"rtB          j                                 ntB          k                                  ||           |D ]} ||                               d
           |r`	 dG                    |l                                          dd                                         pd| }m|m dt                                          d           }n|m                    ||n           n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w	 |o                    |d¦           n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w	 |p                                 n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w|||F                    |           S S t          ||           S # t*          $ r}t          |          jf         dt          |           }ot          r                    d||o           d| d| dt                                          d           d|                     dd           d| d|o dɝ}	d	|	d
|ofcY d}~|r6|!d5k    r!t          j        i                    d4d           n|!t          j        d4<   |"rtB          j                                 ntB          k                                  ||           |D ]} ||                               d
           |r`	 dG                    |l                                          dd                                         pd| }m|m dt                                          d           }n|m                    ||n           n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w	 |o                    |d¦           n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w	 |p                                 n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w|||F                    |           S S t          ||           S d}~ww xY w# |r6|!d5k    r!t          j        i                    d4d           n|!t          j        d4<   |"rtB          j                                 ntB          k                                  ||           |D ]} ||                               d
           |r`	 dG                    |l                                          dd                                         pd| }m|m dt                                          d           }n|m                    ||n           n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w	 |o                    |d¦           n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w	 |p                                 n:# t*          t          f$ r&}t                              d||           Y d}~nd}~ww xY w|||F                    |           w w t          ||           w xY w)a   
    Execute a single cron job.

    ``defer_agent_teardown``: when a caller passes a list, ``run_job`` skips
    the agent's async-resource teardown (``agent.close()`` +
    ``cleanup_stale_async_clients()``) in its ``finally`` block and instead
    appends the live agent to that list. The caller is then responsible for
    calling ``_teardown_cron_agent(agent)`` AFTER it has delivered the result.
    This closes the ordering window in #58720 where delivery ran against a
    torn-down async client (defense-in-depth alongside the interpreter-shutdown
    guard). When ``None`` (the default) teardown happens inline as before, so
    every existing caller is unchanged.

    Returns:
        Tuple of (success, full_output_doc, final_response, error_message)
    r   r   rT  r   no_agentrV  z/no_agent=True but no script is set for this jobr  Fr   workdirNz%Y-%m-%d %H:%M:%Su   ⚠ Cron watchdog 'z' script failed

z

Time: z# Cron Job: z

**Job ID:** z
**Run Time:** z8
**Mode:** no_agent (script)
**Status:** script failed

r%  u8   Job '%s' (no_agent): wakeAgent=false gate — silent runzB
**Mode:** no_agent (script)
**Status:** silent (wakeAgent=false)
Tu0   Job '%s' (no_agent): empty stdout — silent runz?
**Mode:** no_agent (script)
**Status:** silent (empty output)
z#
**Mode:** no_agent (script)

---

r   )AIAgent)	SessionDBz0Job '%s': SQLite session store not available: %sr   z6Job '%s' (ID: %s): wakeAgent=false, skipping agent runu<   

Script gate returned `wakeAgent=false` — agent skipped.
)rR  u=   Job '%s' (ID: %s): blocked by prompt-injection scanner — %sz
**Status:** BLOCKED

The assembled prompt (user prompt + loaded skill content) tripped the cron injection scanner and the agent was NOT run.

**Scanner result:** a  

Audit the skill(s) attached to this job for prompt-injection payloads or invisible-unicode markers. If the skill is legitimate and the match is a false positive, rephrase the content to avoid the threat pattern (`tools/cronjob_tools.py::_CRON_THREAT_PATTERNS`).z6Job '%s': script produced no output, skipping AI call.cron_rU  z%Y%m%d_%H%M%SzRunning job '%s' (ID: %s)z
Prompt: %sd   1HERMES_CRON_SESSION)set_session_varsclear_session_vars_VAR_MAP)r   r   r  )!HERMES_CRON_AUTO_DELIVER_PLATFORM HERMES_CRON_AUTO_DELIVER_CHAT_ID"HERMES_CRON_AUTO_DELIVER_THREAD_IDuG   Job '%s': configured workdir %r no longer exists — running without itTERMINAL_CWD_UNSET_zJob '%s': using workdir %s)load_hermes_dotenvreset_secret_source_cache)r   r  r   r  r   r  r   modelHERMES_MODELzconfig.yamlra  rb  )managed_scopedefaultz8Job '%s': failed to load config.yaml, using defaults: %sr  z%' has no model configured (job.model=z, HERMES_MODEL=ze, config.yaml model.default missing or empty). Set a per-job model via `cronjob action=update job_id=z; model=<name>` or set a default with `hermes model <name>`.)apply_ipv4_preferencenetwork
force_ipv4)force)parse_reasoning_effortr8   reasoning_effortHERMES_PREFILL_MESSAGES_FILEprefill_messages_filerz8Job '%s': failed to parse prefill messages file '%s': %s	max_turnsZ   provider_routing)resolve_runtime_providerformat_runtime_provider_error)	AuthError	requestedr  r  explicit_base_urlz3Job '%s': primary auth failed (%s), trying fallbackapi_keyexplicit_api_keyz!Job '%s': fallback resolved to %sz Job '%s': fallback %s failed: %sprovider_snapshotz
provider 'z' -> 'r  model_snapshotzmodel 'r  u   Job '%s': SKIPPED — global inference config drifted since creation (%s) and this job is unpinned. Skipped to prevent unintended spend. Pin explicitly to proceed: `cronjob action=update job_id=%s provider=<p> model=<m>`.zaSkipped to prevent unintended spend: global inference config drifted since this job was created (z), and this job is unpinned. No inference call was made. To run on the new config, pin it explicitly: `cronjob action=update job_id=zZ provider=<provider> model=<model>` (or pin the original values to keep them). See #44585.)	load_poolz@Job '%s': loaded credential pool for provider %s with %d entriesz3Job '%s': failed to load credential pool for %s: %s)discover_mcp_toolsz"Job '%s': %d MCP tool(s) availablez3Job '%s': MCP initialization failed (non-fatal): %sapi_modeacp_commandcommandacp_argsargsmax_iterationsreasoning_configprefill_messagesfallback_modelcredential_poolproviders_allowedonlyproviders_ignoredignoreproviders_orderorderprovider_sortsortopenrouter_min_coding_score
openroutermin_coding_scorerO   r9   
quiet_modeskip_context_filesload_soul_identityskip_memoryrQ   
session_id
session_dbHERMES_CRON_TIMEOUTz2Invalid HERMES_CRON_TIMEOUT=%r; using default 600sg     @g      @r   r   r   g        get_activity_summaryseconds_since_activityr   last_activity_descr  current_toolapi_call_countz_Job '%s' idle for %.0fs (inactivity limit %.0fs) | last_activity=%s | iteration=%s/%s | tool=%snone	interruptzCron job timed out (inactivity)z' idle for z	s (limit u   s) — last activity: z agent.run_conversation returned z instead of dict: turn_exit_reasonfinal_responsefailed	completedzmax_iterations_reached(r   zagent reported failurezJob '%s' reached the iteration limit but produced a final fallback response; delivering the response instead of failing the cron runz(No response generated)uN   Job '%s': abnormal empty turn (%s) — suppressing explainer for cron deliveryz
**Schedule:** schedule_displayzN/Az

## Prompt

z

## Response

zJob '%s' completed successfullyr   r  zcron u    · z%b %d %H:%Mz.Job '%s': failed to set cron session title: %scron_completez#Job '%s': failed to end session: %sz2Job '%s': failed to close SQLite session store: %sr  zJob '%s' failed: %sz (FAILED)

**Job ID:** z

## Error

```
z
```
r2   )sr:   r   rT   r   r   r   is_dirr@  getcwdchdirrt  rF  _hermes_nowstrftimerQ  r   SILENT_MARKER	run_agentr  hermes_stater  rS   r   r  r-   rU   r   r9  gateway.session_contextr  r  r  rI   _terminal_cwd_lockr   r   hermes_cli.env_loaderr  r  r   r  rA  yamlr3  r/  open	safe_load
hermes_clir  apply_managed_overlayr   r   r   r  hermes_constantsr  r  r,  r-  rK  loadrR   hermes_cli.runtime_providerr  r  hermes_cli.authr  r  r   r   r;   r   agent.credential_poolr  has_credentialsr#   entriestools.mcp_toolr  rW   r?   r   r  r  r{  r   r   r   contextvarscopy_contextr  r  run_conversationrK   r   r  r  r   r  r  r  r  r.   r   #_format_turn_completion_explanationpopr   r   r   set_session_titleKeyboardInterruptend_sessionr  _teardown_cron_agent	exception)pr   r  r  r%   r  r  _job_workdir
_prior_cwdr   outputnow_isoalertdoc
silent_docr  _session_dbr  r   rR  _ran_ok_script_outputrT  	block_excblocked_docr   _cron_session_idr8   r  r  r  _ctx_tokens_cron_delivery_vars	_var_name_prior_terminal_cwd_holds_cwd_writer  r  delivery_targetr  _cfgr  	_cfg_path_fr  
_model_cfg_defaultr  _net_cfgr  r  r  r=   prefill_filepfpath_pfr  prr  r  r  runtime_kwargsruntimeauth_excfb_listr6  	fb_kwargsfb_excrV   message_drift_provider_snapshot_current_provider_model_snapshot_current_model_changesr  r  runtime_providerr  r  r  
_mcp_tools_mcp_exc_raw_cron_timeout_cron_timeout_cron_inactivity_limit_POLL_INTERVAL
_cron_pool_cron_context_cron_future_inactivity_timeoutrK   donerU  
_idle_secs_act	_activity
_last_desc	_secs_ago	_cur_tool_iter_n	_iter_maxr  final_response_textmax_iteration_summary	_err_textr  _explainer_textlogged_response_title_base_cron_title	error_msgsp                                                                                                                   r)   run_jobr^  R	  s&   & YF3776??Ocggh&7&7O6OZPPH( wwz S'ggh'' 	&CCLL555"b#%%
 	**0b7799AT
 	"D..5577 	"J"&&&& " " "!


"	(55JB%HZ((((   D &z%HZ((((   D & --(()<== 	-
#h # ## # # # x  % !( 
     #uf,,  '' 	9KKJF  :x : :%: :!(: : :  ]D88||~~ 		9KKJFSSS7x 7 7%7 7!(7 7 7  ]D888  ! $ 
    	 S&$&& "!!!!! K`******ikk ` ` `GQUWZI[I[]^________` M''(##K 9'44"/ 	9+N;; 	9KKH&  
Nx N N%N N!,!7!78K!L!LN N N  ]D886"3mDDD% 6 6 6
 	Kfi	
 	
 	


T8 
T 
T!
T 
T(]]334GHH
T 
T $-
T 
T 
T 	 k2s9~~5555555-6. ~LhWWWR,,S!!FQvQQ(>(>(O(OQQ
KK+Xv>>>
KKfTcTl+++E
 ),BJ$% WVVVVVVVVV. #"  K

 ) $ $	####$ GGI&&,"3355=L D..5577  	UL	
 	
 	
  *..CC#4/ *((****'')))h0 	L)5BJ~&KK4flKKK	
 	
 	
 	
 	
 	
 	
 	
 	"!###'7'9'9::::2377 	89==oj>YZZZ78<<SQZA[=\=\]]]9:>>"&&{33; 566     CBIn$=$=C 	bKKK,..>??Iw~~i(( -)g666 4">>"--3D4 4 4 4 4 4 4 4 4 4 4 4 4 4 4888888(>>tDDDD    D'-- "XXg..4"
www'' -!*c22 - *#J55 - $.>>)#<#<#W
w@W@W# -$,E 	b 	b 	bNNUW]_`aaaaaaaa	b
 5#&& 		5;;== 		6X 6 6!ggg..6 6 "	." = =6 6
 286 6 6  	>>>>>>xx	2..H(D)) 2hll<.H.H 2%%D1111 	 	 	D	
 	<;;;;;11HHWb!!%%&8"==
 
  -7"8M8Mt-T-T\DHHWb)))Z\	I4b99 :xx/44:}}4b99 	
  	,,''2244F%%'' 5)++f4}} ,,fcG<<< :+/9S>>(: : : : : : : : : : : : : : :%&6== 0+/(  , , ,NN#]_egmopqqq'+$$$$$$,
 '2..22;??^488KCXCX^\^ XX())/R	
 	
 	
 	
 	
 	
 	
 	
 	.----- 	$C(((!	1 SWWZ00N wwz"" J69ggj6I6I23..@@@@GG 	Z 	Z 	ZNNPRXZbccc(..GG  l l
l!,eii
.C.C DIyy,, K9>z9J	"56yy++ I8=i8H	"4566CCCCGKK CVW[[YcMdMdeeeE  l l lLL!CVUYYWaMbMbdjkkkkkkkkl"#@#@#J#JKKQYY  	1 	1 	133C88Gw''S0	1. !gg&9::@bGGIIOOQQ 	swwz':':'@b&G&G&I&I 	 #GKK
$;$;$Ar B B H H J J P P R R  %6:L%L%LO!3OO;LOOO   77#344:AACCIIKK 	CGGG$4$4$:#A#A#C#C 	 "--3355;;==N .O"C"CFoFF^FFF    	yy((HNNL    J7?J J !	J J J   ,D119Tw{{:66<"==CCEEKKMM 	qq;;;;;; y!122'')) &*OKKZ(DLLNN++	    q q qRTZ\lnoppppppppq	999999++--J 8C
OO    	 	 	NNE       	  
 
 
%
KK	***
 [[,,,
 [[,,,	

 [[,,,
  I...
 [[(((
 *>
 .-
 .-
 *>
 ,O
 !ffVnnn
 !ffX...
 FF7OOO
  &&...!
" *.,)?)?)E2(J(JK](^(^(^#
$ <CFFF%
& >dCCC'
( t)
2 $(#5#5553
4  $t5
6 7
8 V9
: (';
< #{=
R I&;R@@FFHH 
	"& %&7 8 8	* & & &H%   !&& "M2?!2C2C'::q:II
 $022!(():E<RTZ[[#	A%-%,,..(055% 6  GD!  !-!4!4!6!6!$Ju&<== !!#(#=#=#?#?D)-2JC)P)PJJ( ! ! ! D!!%;;;.2+#$  	 	 	U4@@@	 U4@@@@JU4@@@@ 	Iu455  % : : < <II    D"';YGGJ!&>BBI!n55Imm$4a88G!&6::ILLA)%;GY#V   uk** C ABBB3X 3 3y>>3 3,/0F,G,G3 3&03 3   &$'' 	f4<<3Hff\bff   vzz*<==CDD%zz*:;;ArHHJJJJx  , *

;''50* ++,EFF* ())	 	 ::h4''FJJ{,C,Cu,L,LUj,L

7## ,&,+ 
 y)))  	NNJ    $4b99?R!!%>>>N !! 	$&6 	$%")"M"MN^"_"_ % % %"$% $>#7#7#9#9_=R=R=T=T#T#Td$  
 "$ -;Y..@Y(    }}%%&9::  ww)511	       	5x@@@V^T18  	A"i//
~t4444-@
>*  	.,,....++---;''', 	( 	(IY##B'''' 	^Z!hhx~~'7'788"=CCEEYIYQWIYIY!,YY+--2H2H2W2WYY--.>LLLL01 Z Z ZMvWXYYYYYYYYZO''(8/JJJJ01 O O OBFANNNNNNNNO^!!####01 ^ ^ ^QSY[\]]]]]]]]^  + $++E2222 ! !////U  , , ,Aww'333q6633	.)DDD(    }}%%&9::  ww)511	        fb)++++++  	A"i//
~t4444-@
>*  	.,,....++---;''', 	( 	(IY##B'''' 	^Z!hhx~~'7'788"=CCEEYIYQWIYIY!,YY+--2H2H2W2WYY--.>LLLL01 Z Z ZMvWXYYYYYYYYZO''(8/JJJJ01 O O OBFANNNNNNNNO^!!####01 ^ ^ ^QSY[\]]]]]]]]^  + $++E2222 ! !////U,4  	A"i//
~t4444-@
>*  	.,,....++---;''', 	( 	(IY##B'''' 	^Z!hhx~~'7'788"=CCEEYIYQWIYIY!,YY+--2H2H2W2WYY--.>LLLL01 Z Z ZMvWXYYYYYYYYZO''(8/JJJJ01 O O OBFANNNNNNNNO^!!####01 ^ ^ ^QSY[\]]]]]]]]^  + $++E2222 ! !////s  9D DD!E 6E 
EEF E54F5
F?FFF1J 
K0KKM, ,
O6AOOOD)Ad 9A_? \0$_? 0\44_? 7\48_? <] _? 
]%"_? $]%%B_? >Ad ?
`/	`*%Ad *`//A.Ad Ac1 0Ad 1
c>;Ad =c>>DAd i1 -ii1 ii1 ii1 0Ad 1
j$;jAd j$$A7Ad Am* (Ad *s01r"Bp20r2
q6<0q1,r1q66rAd s&ssH=Ad A#}( 'Ad (
~2~Ad ~Ad ; Ad 
A@	#A@?Ad @A@	@	FAd FAF) F(Ad F).AGGAd GAGGA'Ad IA(AK1 J+*AK KAK1 K
AK#K AK1 K"AK#K#AK1 K0AL/ K1#ALLAL/ LAd L/AMMAd M!AM6 M5Ad M6
ANN Ad NANNJAd X	AX XAd XAX.X+Ad X-AX.X.B;Ad ^BA``A`?`A`:`:A`?aAaaAba+AbbAbbAb*b*Ac!b;AccAc!dAodBAof*Aof+Ao iBAkkAlkAl l All	Al l Aml1AmmAmmAm0m0An'nAn"n"An'oAooAo oBAxq;BAs>s=Axs>At5tAt0t+Axt0At5t5Axt9AuuAxuAvu!Avu=AxvAvvAxvAv vAxv Awv1AwwAxwAww/Axc                 4   	 | |                                   n:# t          t          f$ r&}t                              d||           Y d}~nd}~ww xY w	 ddlm}  |             dS # t          $ r'}t                              d||           Y d}~dS d}~ww xY w)u  Release an ephemeral cron agent's async resources.

    Split out of ``run_job``'s ``finally`` so a caller that defers teardown
    (to deliver first — #58720) can invoke the identical cleanup AFTER delivery.
    Closes the agent (subprocesses, sandboxes, browser daemons, OpenAI/httpx
    client) and reaps stale async clients whose loop has since closed. Idempotent
    and independently guarded, matching the original inline behavior.
    Nz-Job '%s': failed to close agent resources: %sr   )cleanup_stale_async_clientsz4Job '%s': failed to reap stale auxiliary clients: %s)r  rS   r  rT   r   agent.auxiliary_clientr`  )r8   r  r   r`  s       r)   r  r    s    QKKMMM() Q Q QDfaPPPPPPPPQXFFFFFF##%%%%% X X XKVUVWWWWWWWWWXs,    AAAA& &
B0BBr  r   verboserc  c                \   	 t          | d                   s7t                              d|                     d| d                              dS ddlm}m}m}  | |t                                          }g }	 t          | |          \  }	}
}}n*# t          $ r |D ]}t          || d                     w xY w	  ||           n#  ||           w xY wd}	 t          | d         |
          }|rt                              d	|           |	r|nt          | |          }t          |                                          }|r:|	r8t!          |          r)t                              d
| d         t"                     d}|r]	 t%          | |||          }nH# t&          $ r;}t)          |          }t                              d| d         |           Y d}~nd}~ww xY w|D ]}t          || d                    n # |D ]}t          || d                    w xY w|	r|                                sd}	d}t-          | d         |	||           dS # t&          $ rQ}t                              d| d         |           t-          | d         dt)          |                     Y d}~dS d}~ww xY w)u  Run ONE due job end-to-end: execute → save output → deliver → mark.

    This is the shared firing body extracted from ``tick``'s per-job closure so
    that BOTH the built-in ticker and an external provider's ``fire_due`` (e.g.
    Chronos) run the identical sequence — no duplicated correctness.

    It does NOT decide whether the job is due, claim it, or compute the next
    run — those are the caller's concern (``tick`` advances ``next_run_at``
    under the file lock before dispatch; an external provider claims via the
    store CAS). This function only fires the given job once.

    Returns True if the job was processed (even if the job itself failed —
    failure is recorded via ``mark_job_run``), False only if processing raised.
    r   u6   Job '%s': one-shot dispatch limit reached — skippingr   Tr   )build_profile_secret_scopereset_secret_scopeset_secret_scoper  NzOutput saved to: %su1   Job '%s': agent returned %s — skipping deliveryF)r  r   zDelivery failed for job %s: %szWAgent completed but produced empty response (model error, timeout, or misconfiguration))delivery_errorzError processing job %s: %s)r   rT   r   r   agent.secret_scopere  rf  rg  r   r^  BaseExceptionr  r   r*   r   r   r   r  r  rS   r:   r   r~   )r   r  r   rc  re  rf  rg  _scope_token_deferred_agentsr  r  r  r   _deferred_agentrh  output_filedeliver_contentshould_deliverder   s                       r)   run_one_jobrr    s   o c$i(( 	KKHD	**   4	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 ('&&'7'9'9::
 
 "$	-5<*:6 6 62GV^UU  	 	 	 $4 A A$_c$i@@@@	 .3 |,,,,|,,,, "	A)#d)V<<K @1;???
 18mnn=abegl=m=mO "/"7"7"9"9::N  '' '.G.X.X 'OQTUYQZ\ijjj!& RR%4S/T\cg%h%h%hNN  R R R%(WWNLL!A3t9bQQQQQQQQR $4 A A$_c$i@@@@A#3 A A$_c$i@@@@A  	n>//11 	nGmESY~NNNNt   2CIqAAASYs1vv...uuuuus   A
I ,I ;B C	 'B99C	 =I 	CI B&G; F G; 
G!1GG; GG; I ;H6I 
J+AJ&&J+c                      	 ddl m}   |                                              dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)u@  Best-effort: tell the active scheduler provider the job set changed.

    Called by the consumer surfaces (model tool / CLI / REST) AFTER a
    successful store mutation (create/update/remove/pause/resume) so an external
    provider (Chronos) can re-provision/cancel the affected one-shot via NAS.
    No-op for the built-in (it re-reads jobs.json each tick), so the default
    path is unchanged. Lives here (not in cron/jobs.py) to keep the store free
    of provider imports — avoids an import cycle and keeps jobs.py low-coupling.
    Never raises into the caller.
    r   )resolve_cron_schedulerz!on_jobs_changed notify failed: %sN)cron.scheduler_providerrt  on_jobs_changedrS   rT   r   )rt  r   s     r)   _notify_provider_jobs_changedrw  5  s    =BBBBBB  0022222 = = =8!<<<<<<<<<=s   "& 
AAATsyncc                     t                      \  }}|                    dd           d}	 t          |dd          }t          r-t          j        |t          j        t          j        z             n9t          r2t          j        |	                                t          j
        d           nH# t          t          f$ r4 t                              d           ||                                 Y d	S w xY w	 t!                      } r|st                              d
t%                                          d                     	 t          r8	 t          j        |t          j                   ni# t          t          f$ r Y nVw xY wt          rK	 t          j        |	                                t          j        d           n# t          t          f$ r Y nw xY w|                                 d	S  rHt                              dt%                                          d          t-          |                     |D ]}t/          |d                    d}		 t1          j        dd                                          }
|
rt7          |
          pd}	n1# t8          t:          f$ r t                              d           Y nw xY w|	u	 t?                      pi }tA          |tB                    r|"                    di           ni "                    d          }|t7          |          pd}	n# tF          $ r Y nw xY w r-t                              dt-          |          |	r|	nd           dtB          dtH          f fdd |D             }d |D             }g }g }dtB          dtJ          j&        j'        ffd}|rNtQ                      }|D ]=} |||          }||)                    |           |s|)                    d           >|rOtU          |	          }|D ]=} |||          }||)                    |           |s|)                    d           >d!d|rOtJ          j&        +                    |          D ]q}	 |)                    |,                                           +# tF          $ r:}t          -                    d|           |)                    d           Y d}~jd}~ww xY w              t]          |          t          r8	 t          j        |t          j                   ni# t          t          f$ r Y nVw xY wt          rK	 t          j        |	                                t          j        d           n# t          t          f$ r Y nw xY w|                                 S |rEt-          |          gdtJ          j&        j/        ddffd }|D ]}|0                    |           n
              t]          |          t          r8	 t          j        |t          j                   ni# t          t          f$ r Y nVw xY wt          rK	 t          j        |	                                t          j        d           n# t          t          f$ r Y nw xY w|                                 S # t          r8	 t          j        |t          j                   ni# t          t          f$ r Y nVw xY wt          rK	 t          j        |	                                t          j        d           n# t          t          f$ r Y nw xY w|                                 w xY w)"u  
    Check and run all due jobs.
    
    Uses a file lock so only one tick runs at a time, even if the gateway's
    in-process ticker and a standalone daemon or manual tick overlap.
    
    Args:
        verbose: Whether to print status messages
        adapters: Optional dict mapping Platform → live adapter (from gateway)
        loop: Optional asyncio event loop (from gateway) for live adapter sends
    
    Returns:
        Number of jobs executed (0 if another tick is already running)
    Tr  Nwra  rb  r   u0   Tick skipped — another instance holds the lockr   z%s - No jobs duez%H:%M:%Sz%s - %s job(s) duer   HERMES_CRON_MAX_PARALLELr   z?Invalid HERMES_CRON_MAX_PARALLEL value; defaulting to unboundedrQ   max_parallel_jobsz.Running %d job(s) in parallel (max_workers=%s)	unboundedr   r   c                 *    t          |           S )u   Run one due job end-to-end. Thin wrapper around the shared
            module-level ``run_one_job`` so ``tick`` and external providers
            (Chronos ``fire_due``) use the identical execute→save→deliver→mark
            body.rb  )rr  )r   r  r   rc  s    r)   _process_jobztick.<locals>._process_job  s    
 sXD'RRRRr+   c                 d    g | ]-}|                     d           pd                                +|.S r  r   r   r   rD   js     r)   rF   ztick.<locals>.<listcomp>  s:    SSS1553C3C3Ir2P2P2R2RS1SSSr+   c                 d    g | ]-}|                     d           pd                                +|.S r  r  r  s     r)   rF   ztick.<locals>.<listcomp>  s:    UUUqQUU95E5E5K4R4R4T4TUUUUr+   r  c                    | d         }t                      r1t                              d|                     d|                     dS t          5  |t
          v r=t                              d|                     d|                     	 ddd           dS t
                              |           ddd           n# 1 swxY w Y   t          j	                    }| |ffd	}	 |
                    |          S # t          $ r}t          |          rnt          5  t
                              |           ddd           n# 1 swxY w Y   t                              d|                     d|                     Y d}~dS  d}~ww xY w)a'  Submit a job fire-and-forget with the in-flight dedup guard.

            Returns the future, or None if the job was skipped because a prior
            tick's run of the same job is still in flight.  The running-set
            membership is released in the worker's finally block.
            r   u8   Job '%s' not dispatched — interpreter is shutting downr   Nu%   Job '%s' already running — skippingc                 8   	 |                     |           t          5  t                              | d                    d d d            S # 1 swxY w Y   S # t          5  t                              | d                    d d d            w # 1 swxY w Y   w xY w)Nr   )r  _running_lockr   discard)r  ctxr  s     r)   _run_and_releasez:tick.<locals>._submit_with_guard.<locals>._run_and_release  s   :77<33& : :(004999: : : : : : : : : : : : : : : : : :(004999: : : : : : : : : : : : : : : :s@   A !AA	A	B !BBBBBB)r   rT   rU   r   r  r   r   r  r  r  r  r  r  )r   r  r  _ctxr  
submit_errr  s         r)   _submit_with_guardz tick.<locals>._submit_with_guard  sC    YF *++ NGGFF++   t - ----KK GQWY_I`I`aaa- - - - - - - - !$$V,,,	- - - - - - - - - - - - - - -
 +--D#&D : : : : : :{{#3444    .j99  & 9 9(008889 9 9 9 9 9 9 9 9 9 9 9 9 9 9NNR//    44444sZ   :B=B==CC$C9 9
FFE 4F E	FE	2F FFc                      	 ddl m}   |              d S # t          $ r&}t                              d|           Y d }~d S d }~ww xY w)Nr   )_kill_orphaned_mcp_childrenz'Post-tick MCP orphan cleanup failed: %s)r  r  rS   rT   r   )r  _es     r)   _sweep_mcp_orphansz tick.<locals>._sweep_mcp_orphans  sy    LFFFFFF++----- L L LFKKKKKKKKKLs    
A?AzCron job future failed: %sFr-  c                    dxx         dz  cc<   	 |                                  }|2t                              d|t          |          ||j        f           n# t
          $ r Y nw xY wd         dk    r              d S d S )Nr   r   z(Cron job future failed in async mode: %srS  )r  rT   r   r  __traceback__rS   )r-  _exc
_remainingr  s     r)   _on_doneztick.<locals>._on_done  s    1"<<>>D'%OQUaefjakakmqsw  tF  aG  H  H  H    Da=A%%&&((((( &%s   AA 
A)(A)r   )1r   r*  r  fcntlflockLOCK_EXLOCK_NBmsvcrtlockingfilenoLK_NBLCKrt  IOErrorrT   r   r  r}   r   r  r  LOCK_UNLK_UNLCKr#   r   r@  rA  r   r  r  r{  rU   r
   r   r   r   rS   r   r   r   r   r   r;   r   as_completedrK   r   sumFutureadd_done_callback)rc  r  r   rx  r   	lock_filelock_fddue_jobsr   _max_workers_env_par_ucfg_cfg_parsequential_jobsparallel_jobs_results_all_futuresr  seq_poolfutr  r_  rV   r  r-  r  r  r  s   ```                      @@@r)   tickr  G  s    *++HiNN4$N/// G
y#888 	AK!>???? 	AN7>>++V_a@@@W   GHHHMMOOOqq	J>> 	8 	KK*KMM,B,B:,N,NOOOv  		GU]3333W%    	w~~//!DDDDW%   G  	aKK,kmm.D.DZ.P.PRUV^R_R_```  	( 	(CSY'''' '+	^y!;R@@FFHHH 5"8}}4I& 	^ 	^ 	^NN\]]]]]	^#+-7t-D-DLEIIfb)))"#)**  '#&x==#8DL     	KK@H ,=+  	Sd 	St 	S 	S 	S 	S 	S 	S 	S 	S TShSSSUUHUUU.	D .	
0B0U .	 .	 .	 .	 .	 .	l  	*+--H& * *((h77;##C((( *OOD)))  	*%l33D$ * *((d33;##C((( *OOD)))	L 	L 	L 	L  
	!  '44\BB + ++OOAHHJJ////  + + +LL!=sCCCOOE********+    x==6  		GU]3333W%    	w~~//!DDDDW%   A  	!l++,J	)Z/6 	)4 	) 	) 	) 	) 	) 	) 	) # / /$$X..../    8}} 		GU]3333W%    	w~~//!DDDDW%     		GU]3333W%    	w~~//!DDDDW%   sV  A?B2 2AC76C7;AZ4 E0 0FF2G GG/A&Z4 :J Z4 +J?<Z4 >J??Z4 A#L) (Z4 )
L63Z4 5L66EZ4 'R*)Z4 *
S.40S)$Z4 )S..Z4 T11UU2VVV/AZ4 X66Y
	Y
2ZZZ4]=[][1.]0[11
]<2\/.]/] ]]]__main__)rc  r   r  )NN)TNNT)r1   r  atexitconcurrent.futuresr   r  rK  loggingr@  r    r1  r7  r   r   r  ImportErrorr  r  r   typingr   r   r   r3  r|  r:   __file__r8  r  r   hermes_cli._subprocess_compatr	   hermes_cli.configr
   r   hermes_cli.fallback_configr   hermes_timer   r  	getLoggerr.   rT   r   r*   rS   r-   rR   r?   rM   rW   	frozensetr9  r<  rB  rq  r}   r~   r   r   r   r  r   r   r   r   r   r   __annotations__r   r  rI   r   r   r  r   r   r  r   r   r   registerrj  r   r   r   rr  r   r   r   r   r   r  r  r  r-  r7  r:  r>  rD  rI  rL  r]  rm  ru  ry  r|  r  r  r  r  r  r  r  r  r  r  r  rF  rQ  r  rv  r  r^  r  rr  rw  r  r2   r+   r)   <module>r     s                 				 				      



    LLLL   E   	       & & & & & & & & & &
 33ttH~~,344 5 5 5 , , , , , , < < < < < < ; ; ; ; ; ; ; ; 9 9 9 9 9 9 * * * * * *		8	$	$29d 293: 29# 29 29 29 29j
 
 
 
 
 
 
 
 $s)    .d3i d tCy    > 4 DI<L    D &I ' ' '    ' % !	
 # + 
 ! ' # ! # - ! '  3! 0 +   d c c c c c c c c c c c c c
  !y!O!O!OPP  C  D        N CG+>? F F F,0 HSM 0 0 0 #   	   EI (:-@A H H H0$ 0$ 0$ 0$ 0$ 0$ 0$ 0$j $^%% HSM j6H6[    j0C    "	  	  	  	  ' ( ( ( H]$; t    B !dTk      	-$ 	- 	- 	- 	--tTz* - - - - (4.    , t (4. D    @4  c &.sm8<   L  $!C
 C
 C
 C
	C
C
 C
 	C

 }C
 c]C
 C
 
C
 C
 C
 C
L"	" "
 c]" " " "X  $J
 J
	J
 J
 	J

 J
 J
 }J
 
J
 J
 J
 J
l  $] ] ]	] ] 	]
 ] ] c]] }] 
] ] ] ]@4T 4c 4 4 4 40     &
,s 
,t 
, 
, 
, 
,
& 
& 
& 
& 
& 
&
C 
C 
 
 
 
c hsm    4  $&tDz & & & &RS Sc Shtn S S S Sl    0 )UG$$ c    $4 DJ    B+$ +8D> + + + + iIIIJJiBBBCC 5g 5g5g 5g Tk	5g 
5g 
5g 5g 5g 5gp1d 1 1 1 1*555 5 	5
 
5 5 5 5px x x x(SV- x x x xv  )#S # # # #By8 y8tSy)9 y8 y8 y8 y8x4C 4D 4 4 4 44HO HO4 HO HO3 HO HO HO HO^ #!%C C CC	C 	C
 C #C 	C C C CL+MT +Md +M +M +M +M^ :>F0 F0 F0	F0(0F0
4c8C=()F0 F0 F0 F0RX X X X X X2 (,$ ~ ~ ~T ~ ~RV ~ ~ ~ ~B= = = =$j j$ jt js j j j jZ zD s3   : AAAAAAAA