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) で実行を1件記録run_job(cron/scheduler.py:2659) で実際に走る:スクリプト型は_run_job_script:2113、セッション (session) 型はrun_conversation:588に渡す
- 実行中のクラッシュ → 次回
recover_interrupted_executions(cron/executions.py:156) が回復マークを付ける。 - 結果はゲートウェイ経由で配信(または指定プラットフォームにミラー)。
スケジューラの 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 injection の盲点:実行時に assembled prompt(skill 内容を含む)をスキャンする。悪意ある skill はスケジュールされるその瞬間まで検出されず、ジョブ作成時には見えない。
まとめ
Cron サブシステムの重点は「無人運転の安全性」:toolsets 隔離、lifecycle_guard による誤操作防止、execution 状態機械によるクラッシュ回復。スケジュール自体はゲートウェイ心拍で駆動し、外部 cron デーモンを必要としない。