Cron 定時
職責
讓 agent 能「到點自動跑」:定時報告、備份、簡報等不必人在場。cron/ 子系統提供排程器、作業儲存、執行記錄、生命週期守衛、藍圖/建議目錄,以及獨立的 toolsets 隔離(定時任務可停用部分工具 (tool),防止無人值守時幹危險事)。網關 (gateway) 用 60s 心跳驅動排程器。
設計動機
無人值守是這套子系統的核心問題。agent 在凌晨三點自己跑起來,誰盯著?Hermes 的回答是「排程鏈路上層層降權」:toolsets 預設裁掉互動類和能再開 cron 的 cronjob;lifecycle_guard 把腳本裡 systemctl restart hermes-gateway 這類命令攔在排程前;executions 表給每次執行留可查的痕跡,崩潰後標 unknown 而不是冒充成功。讓「agent 自己跑」能稽核、能回滾。
關鍵檔案
def run_job:2659— 執行單個作業的核心_run_job_script / _run_job_script_with_claim_heartbeat:2113-2247— 腳本型作業執行 + 租約心跳執行態與中斷:352-445—get_running_job_ids/mark_running_jobs_interrupted/_consume_interrupted_flagtoolsets 解析:143-210—_resolve_cron_disabled_toolsets/_resolve_cron_enabled_toolsets(定時任務的工具隔離)執行緒池:501-547—_get_parallel_pool/_get_sequential_pool_CronStorePaths / _current_cron_store:106-157— 作業儲存路徑鎖與輸出目錄:251-360—_jobs_lock_file/_job_output_dir_normalize_job_record:426-461— 作業記錄標準化執行狀態機:99-156—create_execution/mark_execution_running/finish_executionrecover_interrupted_executions:156-213— 崩潰恢復 +list_executions/latest_executioncheck_gateway_lifecycle:69-112— 防止定時腳本誤操作網關生命週期blueprint_catalog— 作業藍圖目錄suggestion_catalog/suggestions— cron 建議生成_start_cron_ticker:22335— 網關內 60s 心跳驅動排程
資料流
- 網關啟動
_start_cron_ticker:22335,每 60s 觸發排程器。 - 排程器讀作業儲存(
cron/jobs.py:106),按 cron 表達式選出到期作業。 - 每個到期作業:
- 經
check_gateway_lifecycle:112掃描腳本,阻止誤改網關生命週期 - 解析該作業的 toolsets(
cron/scheduler.py:210),通常停用危險工具(見子代理的 auto-deny 同源思路) - 在
create_execution(cron/executions.py:99)記一筆執行 run_job(cron/scheduler.py:2659)實際跑:腳本型走_run_job_script:2113,會話型餵給run_conversation:588
- 經
- 執行中崩潰 → 下次
recover_interrupted_executions(cron/executions.py:156)恢復標記。 - 結果經網關投遞(或 mirror 到指定平台)。
排程器對 toolset 的裁剪硬編碼三項,疊加使用者 config.yaml 的 agent.disabled_toolsets:
def _resolve_cron_disabled_toolsets(cfg: dict) -> list[str]:
"""Toolsets a cron-spawned agent must never receive."""
disabled = ["cronjob", "messaging", "clarify"]
agent_cfg = (cfg or {}).get("agent") or {}
user_disabled = agent_cfg.get("disabled_toolsets") or []
for name in user_disabled:
name = str(name).strip()
if name and name not in disabled:
disabled.append(name)
return disabledcronjob 被禁是不讓 cron 自己再造 cron(誤觸能級聯開一堆作業),messaging/clarify 是互動類,無人值守只會掛著等不到人。腳本型作業走 _run_job_script,路徑被關在 HERMES_HOME/scripts/ 裡防 traversal。
執行狀態機在 executions.py,狀態只有 claimed → running → completed/failed/unknown,終態只能寫一次(finish_execution 的 UPDATE 加 WHERE status IN ('claimed','running') 保護,重複寫直接 rowcount=0 回傳 None)。崩潰恢復只把「程序已不存在」的執行標成 unknown,不假裝成功也不自動重跑——子程序可能已經寫了副作用,也可能沒,留給操作者去查,而不是猜 failed 讓人以為重跑就完事。lifecycle_guard 把 systemctl restart hermes-gateway、pkill hermes gateway 編成正則,建立時和執行前各掃一次,把 prompt 和 script 拼起來掃,堵住「prompt 裡說一句、腳本裡幹一刀」的拆分繞過。
邊界與失敗
- 秒級漂移與誤傷:60s 心跳意味著觸發點最多差一分鐘。
mark_running_jobs_interrupted不區分哪個 PID 被殺,凡在殺的時刻在跑的作業統統打斷,可能誤傷快收尾的作業。 unknown狀態的副作用:崩潰恢復標unknown後不重跑,但腳本可能已發過訊息/寫過檔案。下游不能假設「未完成」。- 租約心跳卡死:
_run_job_script_with_claim_heartbeat用獨立執行緒續租,心跳執行緒本身卡住(如 GIL 長持有)時,另一個排程程序可能把同一個 one-shot 作業再派一次,產生重複副作用。 - prompt 注入盲區:執行時才掃 assembled prompt(含 skill 內容),惡意 skill 要等排程那一刻才被攔下,建立作業時看不到。
小結
Cron 子系統的重點是「無人值守的安全性」:toolsets 隔離、lifecycle_guard 防誤操作、execution 狀態機做崩潰恢復。排程本身由網關心跳驅動,不需要外部 cron 常駐程式。