P33 33_进化篇_Cron定时任务
P33 · 33_进化篇_Cron定时任务(12:59)
课时概要
进化篇 · Cron 定时任务。官方 README「Scheduled automations」:Built-in cron scheduler with delivery to any platform —— 每日报告、夜间备份、每周审计,自然语言描述、无人值守运行,结果可投递到任意平台。
本分P 没有逐P 的官方简介(视频简介只有资源链接)。以下「本节要点 / 关键概念 / 代码实操」均改写自 Hermes 官方文档 Cron 页(含 preflight / 执行台账 / 失败事件 / 投递 等节)与官方仓库原文并标注来源;「我的收获」里标 本机实测 的是在本机实跑得到的真实状态。
视频:33_进化篇_Cron定时任务 | 时长 12:59 | P33
本节要点
- cron 现在能做什么(官方清单):① 一次性或重复任务 ② 暂停/恢复/编辑/触发/删除 ③ 挂 0、1 或多个 skill ④ 投递回原会话 / 本地文件 / 已配置的平台目标 ⑤ 在全新的 agent 会话里跑(带常规静态工具列表)⑥ no-agent 模式 —— 就是个脚本按计划跑,stdout 原样投递,零 LLM 参与 ⑦ 外部事件触发 —— webhook 路由带
cron_job时,事情一发生就触发,不用等下一个调度 tick。 - 全部能力也通过
cronjob_manage工具交给 Hermes 自己 —— 所以可以直接用自然语言让它建、暂停、编辑、删除任务,不用碰 CLI。 - 任务跑在哪个模型上?三级解析(官方原文):① 每任务 pin → ②
config.yaml的cron.model→ ③hermes model的主 agent 模型。 · 每任务 pin:--model/--provider显式指定,或--pin锁定当前主模型(--unpin解锁)。⚠️ agent 不能把任务指到别的模型 —— 「推理 pin 属于用户所有」。 ·cron.model是整个 cron 舰队的默认模型:设一次之后,用hermes model//model切聊天模型永远不会动到 cron 舰队。 · 都没设时才跟随主模型 —— 改主模型,所有未 pin 的任务下次运行就跟着变。 - 每任务推理档位:
none / minimal / low / medium / high / xhigh / max / ultra,设了就覆盖全局agent.reasoning_effort与每模型覆盖(none关掉思考)。用法举例:重的定期分析跑high、便宜的周期性任务跑minimal,不动全局默认。⚠️ 官方刻意不把它暴露给 agent 的cronjob_manage工具(模型配置是用户的决定);模型不支持的档位会被 provider 夹/省略;对no_agent任务无效(没有 LLM 调用可调)。 - ⚠️ cron 运行里的会话不能递归创建更多 cron 任务 —— Hermes 在 cron 执行里禁用了 cron 管理工具,防止失控的调度循环。
- 三种创建方式:聊天里
/cron add "in 30m" "提醒我检查构建"(也支持every 2h、--skill x --skill y)|hermes cron create "every 2h" "…"|自然语言(「每天早上 9 点检查 Hacker News 的 AI 新闻并发我 Telegram」)。 - 预派发配置校验(
cron.preflight,默认开):在构造任何 agent 机制之前,调度器先校验任务配置真能跑出成功的一轮 —— ① provider API key 能解析 ② 挂的技能就绪(所需环境变量/命令/凭证文件都在)③ 投递平台目标已知且有 gateway 凭证(local/origin不检查)④ 任务自己enabled_toolsets里点名的每个 MCP server 至少解析出一个工具。 · 校验失败 →last_status: blocked_config、只告警一次(不是每 tick 都告)、完全不调 LLM(配置错的任务永不烧 token);下次健康运行会清掉 blocked 状态,这样未来的新问题还能再告警。 · 官方提示:provider credential missing: … [profile 'default', HERMES_HOME /opt/data]这种消息会点名它读的 profile 与 HERMES_HOME —— 交互会话里能用的凭证,gateway 读的可能是另一个auth.json/.env(Docker 的HOMEvsHERMES_HOME、service unit、多路复用卫星 profile 都会造成这种差异)。 - 技能支撑的任务(skill-backed):任务可以在跑 prompt 之前加载一个或多个 skill,按顺序加载,prompt 是叠在这些技能之上的任务指令;每个技能都按
/skill-name的方式加载(含[Skill config …]块)。意义:让计划任务继承可复用工作流,而不用把技能正文塞进 cron prompt。 - 在项目目录里跑任务(
--workdir/workdir=):cron 任务默认脱离任何 repo —— 不加载AGENTS.md/CLAUDE.md/.cursorrules,terminal/file/code-exec 工具从 gateway 启动时的工作目录跑。设了 workdir 之后:① 该目录的上下文文件被注入 system prompt(发现顺序同交互 CLI)②terminal/read_file/write_file/patch/search_files/execute_code全部以它为工作目录。⚠️ 路径必须是绝对且存在的目录(相对路径与不存在的目录在创建/更新时就被拒);--workdir ""清除。 · 隔离:每次运行把 workdir 绑到该次运行的唯一任务身份 → 因此能用正常并行池而不污染进程级终端状态、也不会在并发运行间泄漏路径。 - 生命周期动作:
pause(保留任务但停止调度)|resume(重新启用;重复任务在暂停期间到期的那个槽位仍然算欠着 → 下次 tick 打一次补跑,而不是静默跳到下一个发生点)|run(下次 tick 触发)|remove|edit|status|tick。 · 名字查找:所有变更动词都接受任务名(大小写不敏感),精确 ID 优先;⚠️ 同名歧义会拒绝并打印候选 ID 让你消歧 —— 因为「名字不唯一」,这个守卫是承重的(防止静默改错任务)。 hermes pause= 全局紧急停止(hermes resume解除):暂停期间没有任何调度触发会启动 —— 内置 ticker 跳过派发、托管调度器的 fire webhook 返503+Retry-After: 60(你恢复后调度器会重投)、错过补偿扫描也待命而不是强制补投。已经在途的运行永不被杀,恢复后第一轮 tick/扫描就补上。显式的操作员手动运行(hermes cron run、仪表盘 Trigger)在暂停时仍会执行。- 创建即暂停(安全金丝雀):
--paused --paused-reason "待审阅"→ 在第一次加锁写入里就存下enabled: false/state: paused/next_run_at: null+ 暂停时间戳 + 可审计原因,且不注册触发器 —— 避免「先创建、再暂停」之间那一段调度竞态。--paused-reason必须与--paused同用。 - agent 管理调度(cron 任务管理 cron 任务):默认禁止(调度出的 agent 不能用
cronjob_manage),要cron.allow_agent_scheduling: true才开。开启后计划任务能像聊天一样管理 cron 表(排后续一次性任务、调自己的节奏、跑一个「cron 管理员」任务来统一对账)。两条性质保证它不失控: · 一张扁平、用户所有的表 —— 从 cron 运行里建的任务跟其他任务落在同一个jobs.json,没有特殊归属,你可以照常 list/edit/remove。 · 没有悬空投递 —— cron 运行是短暂的,所以运行内部的deliver: origin在创建时就被解析成创建者自己的具体目标(platform:chat_id[:thread_id],或创建者不投递时解析成local)→ 计划 agent 建的任务永远不可能把输出指向一个已经不存在的会话。 · 「盯着 X、只告诉我一次、然后停」这个模式可以成立:任务可以在运行里remove自己然后仍然汇报(最终响应照常投递、运行记为completed)。 - 运行机制:cron 由 gateway 守护进程负责;gateway 每 60 秒 tick 一次,把到期任务跑在隔离的 agent 会话里。每次 tick:从
~/.hermes/cron/jobs.json载入任务 → 用当前时间比对next_run_at→ 为每个到期任务起一个全新的AIAgent会话 → 可选注入挂的技能 → 把 prompt 跑到完成 → 投递最终响应 → 更新运行元数据与下次计划时间。~/.hermes/cron/.tick.lock文件锁防止重叠 tick 重复跑同一批任务。 - systemd 下的重启安全 worker:当 gateway 作为 systemd 服务运行时,每个到期任务被交给一个在瞬态用户 scope(
systemd-run --user --scope)里启动的外部 worker 进程 → 跑任务中途重启 gateway 不会杀掉任务。 · 没有用户 systemd 会话的宿主(容器、精简 LXC、没开 linger 的服务账号)提供不了这个 scope → 默认降级:任务仍作为独立外部进程跑、交接方式相同,但没有 cgroup 隔离,此时重启 gateway 会杀掉它(执行台账会记录),且每个 gateway 进程记一条警告。 · 想改成 fail closed(跳过任务并把错误记到任务行)就设cron.require_restart_safe_scope: true;根治是给 gateway 用户开用户会话:sudo loginctl enable-linger <gateway-user>。 · worker 就是 gateway 自己的解释器跑python -m cron.scheduler,PYTHONPATH 上钉着 gateway 的 checkout → 导入的是同一棵 Hermes 树。worker 在确认交接前就死掉的话,它自己的 stderr 尾巴会被记进任务的上次错误与执行台账 —— 所以失败的 import 会被点名,而不是只留一个裸退出码。 - 执行台账
~/.hermes/cron/executions.db(在派发给 executor 或 provider 之前就记录):每次被认领的尝试走过claimed→running→ 一个不可变终态:completed/failed/unknown。重启之后、以及每次手动hermes cron run//cron run之前(这样没有调度器时的一次性调用也能自愈台账),Hermes 只在原 PID 与进程启动指纹都证明其拥有者已消失时才把被遗弃的尝试标成unknown。⚠️unknown是审计记录,永不自动重跑。 · 查:hermes cron runs [job-id] --limit 20(别名history)。终态历史有界,活跃尝试永不剪枝;台账包含在快速备份里。 · 计划尝试还会单独记录它的精确计划时刻(与认领时刻分开)→ 如果一份旧的jobs.json快照重新武装了一个台账里已记为完成的 occurrence,Hermes 会跳过这次重放并重新锚定重复任务。 · ⚠️ 官方明说:这不是 exactly-once 的副作用保证 —— 没有身份的旧行、被剪枝的历史、不可用的台账、被中断的尝试都无法证明完成。 - 重复失败复查提醒:每个任务跟踪
failure_streak(连续失败次数;投递失败不计)。在 agent 都还没被触达前就失败的一轮(更新只打了一半导致 import 坏掉、provider client 构造不出来)照样计数并告警。重复任务的 streak 达到阈值(cron.failure_nudge_threshold默认 3)时,投递到聊天的失败消息里会附带一条复查提醒(建议修/暂停hermes cron pause <job>/删除);任何成功运行都重置 streak;hermes cron list会在失败任务旁显示 streak。一次性任务永不提醒。 - 模型不可达时的自动重跑:重复任务若在一次模型调用都没发出前就因瞬态网络/DNS 错误失败(经典场景:电脑刚唤醒、VPN 或 Wi-Fi 还在重连),不会白等一整个周期 —— 调度器会在 5、15、30 分钟后自动重跑,然后回到正常计划。因为零 API 调用,重跑是花费中性的、也不可能重复任何副作用。重跑待定期间中间那条失败通知被抑制(重跑成功就给你真结果,梯子用完才给正常失败告警);任何触达模型的运行都重置梯子;一次性任务被排除在外。
cron.retry_unreachable: false关闭。 - provider 用量窗口关闭时挂起任务:镜像情形 —— provider 明确告诉你它还要关多久(目前是 OpenAI Codex 的用量探测),返回用量耗尽 +
retry after <N>s,而整条 fallback 链也不可用时,对着这个窗口反复重触发亚小时级任务是保证每次同样失败、且每次都告警的。于是调度器停放该任务:那条唯一的失败告警说明窗口关闭、任务被挂起,next_run_at移到窗口之后第一个计划时刻(任务记录上的quota_hold_until),在那之前什么都不触发也不告警。(模型 API 在运行中途返回的 429 不走这条,它按正常节奏重试。) - 失败事件(incident):告警一次、冷却后提醒、可确认:一直用同样错误失败的重复任务只告警一次,不是每次运行都告。每次失败被记成一个耐久事件,键 = 任务 + 错误文本的归一化签名;某签名的首次失败必定投递,之后在事件处于
alerted期间重复被压住(运行仍被记录 ——hermes cron runs与失败 streak 都看得到,只是不 ping)。 ·cron.failure_repeat_alert_hours(默认 6):还坏着就补一条提醒然后再次静默;设 0 = 每次失败都告警。 · 任何让情况变化的事都立即告警:换了错误就新建自己的事件并立刻 ping;一次成功会重新武装该签名(绿色运行之后同样的错误会再次告警)。事件台账读不出来时,宁愿投递 ping 也不吞掉。 · 命令:hermes cron incidents(列事件,最新活动在前)|--state detected|alerted|resolved|closed|hermes cron incidents ack <id>(确认 = 对该签名永久静默,提醒也含;其余不变,运行历史照记、streak 照数、换个错误照告警)。成功会把该任务所有开放事件标resolved;已确认(closed)的例外 —— 成功不管它、重复也保持静默。 hermes cron doctor(舰队体检,只读):逐任务检查并在有任何发现时退出码 1(包括历史上的迟到/补跑派发)。检查项:last_status非 ok(带记录的错误)|上次投递失败(输出产生了但从没送到你手上)|上次派发是迟到或补跑(这个警告要等下一次准时触发才清 —— 所以宿主机唤醒后,「补跑成功」也不会立刻清掉迟到警告)|某次计划触发没能到达 runner |next_run_at缺失,或停在过去超过 15 分钟宽限窗口 ← 这就是「任务在静默地不触发」的信号(调度器死了、gateway 挂了、或触发认领卡住)|脚本缺失/不是文件/解析到HERMES_HOME/scripts之外 |no_agent任务没有脚本 |配置的workdir已不存在。doctor 永不改动任务或状态,只报告;深挖时配hermes cron incidents(耐久失败记录)+hermes cron runs(尝试台账)。- 投递选项:
origin(回创建任务的地方;消息平台默认)|local(只存~/.hermes/cron/output/;CLI 默认)|平台名(该平台 home channel)|平台:chat_id[:thread_id](精确目标/话题)|bot-chat[:profile](把输出投进某个 profile 的「Bot Chat」会话 —— 收件人是 bot 自己,它会把这当一条真消息去处理和回应;每次投递花目标 bot 一整轮 agent,所以注意频率)|all(触发时解析成所有配了 home channel 的平台 —— 所以在你接上 Telegram 之前建的任务,下一个 tick 就会带上 Telegram)|逗号组合(telegram,discord)与与all组合(origin,all,按(platform, chat_id, thread_id)去重)。 · agent 的最终响应会被自动投递到配置的deliver:目标 —— agent 自己不发送消息,所以 cron prompt 里没有东西要调。 · 投出的输出每条通道都做密钥脱敏:凭证形状(厂商前缀 API key、token、KEY=value)即使security.redact_secrets: false也会被遮 —— 那个设置管的是你自己的日志,不是离开这台机器的东西;脱敏器失败时替换掉载荷而不是未扫描就发出。cron/output/<job_id>/下的运行文档保留 agent 原样的响应。 - ⚠️ 投递失败是一个独立状态:执行与投递分开追踪。agent 运行成功但输出没到达目标(平台 5xx、限流、会话过期、适配器没返回发送成功的正面证据)时,任务记
last_status: delivery_failed(绝不是普通的ok),原因写在last_delivery_error;hermes cron list用黄色显示delivery_failed: <原因>,doctor报为投递问题,手动cronjob run返回success: false+ 投递错误。投递失败不计入failure_streak(agent 干完活了),下一次完全成功的运行把状态恢复成ok。 hermes cron status会点名「迟到」:next_run_at已经比现在晚 15 分钟以上过期时,永远不会被当作「下次运行」展示,而是打印⚠ Next run <time> is OVERDUE — passed 7h ago but the job has not fired;cron list与聊天里的/cron list把该行标Overdue:;仪表盘与 Desktop 的 cron 面板显示Overdue since;调度器停摆时status(gateway 挂掉的情况下)与仪表盘 Cron 页还会说它最后一次 tick 是什么时候。官方称这是「调度器停止 tick 的特征」→ 重启 gateway(hermes gateway restart),或立刻hermes cron run <id>跑掉它。
关键概念
- 三级模型解析:每任务 pin →
cron.model→ 主 agent 模型。 cron.preflight:派发前校验(provider key / 技能就绪 / 投递目标 / MCP 工具解析),失败即blocked_config且不烧 token。- skill-backed job:任务里挂 1..N 个技能,按顺序加载后再跑 prompt。
workdir:把任务钉在某个项目目录(上下文文件 + 工具工作目录都跟着走)。hermes pause:全局紧急停止(手动 run 仍可执行)。- 执行台账:
claimed/running/completed/failed/unknown,unknown永不自动重跑。 failure_streak/ incidents:连续失败计数 + 按错误签名归并的耐久事件(告警一次 → 冷却提醒 → 可 ack 永久静默)。- 补跑(catch-up)与迟到:暂停/停摆期间错过的槽位会补一次;迟到警告要到下次准时触发才清。
- 重启安全 worker:systemd 瞬态 scope(
systemd-run --user --scope),没有就降级或 fail closed。 - 投递目标与 delivery_failed:执行成功 ≠ 投递成功,两者分开记。
no_agent模式:纯脚本 + stdout 原样投递,零 LLM。
代码 / 实操
yaml
# ~/.hermes/config.yaml —— cron 相关键(官方原文)
cron:
model: <name> # 整个 cron 舰队的默认模型(不动聊天模型)
preflight: true # 派发前校验;false 恢复旧行为(跑到中途才失败)
allow_agent_scheduling: false # 允许 cron 任务管理 cron 任务
catch_up_missed: true # 暂停期间错过的槽位是否补跑
require_restart_safe_scope: false # 没有 systemd scope 时降级(true=跳过任务)
max_parallel_jobs: <n> # 限制 cron 总并发
failure_nudge_threshold: 3 # 连续失败几次后附带复查提醒(0=关)
failure_repeat_alert_hours: 6 # 同样错误冷却多久后再提醒一次(0=每次都告)
retry_unreachable: true # 模型不可达时 5/15/30 分钟自动重跑bash
# 创建 / 编辑
hermes cron create "every 2h" "Check server status"
hermes cron create "every 1h" "Combine both skills" --skill blogwatcher --skill maps --name "Skill combo"
hermes cron create "every 1d at 09:00" "Audit open PRs" --workdir /home/me/projects/acme
hermes cron create "every 1h" "Post the digest" --paused --paused-reason "Awaiting review"
hermes cron edit <job_id> --schedule "every 4h"
hermes cron edit <job_id> --pin # 锁定当前主模型
hermes cron edit <job_id> --reasoning-effort high # 每任务推理档位
# 生命周期与体检
hermes cron list | status | doctor
hermes cron pause|resume|run|remove <id_or_name>
hermes cron runs [job-id] --limit 20 # 执行台账
hermes cron incidents [--state alerted] # 失败事件
hermes cron incidents ack <id> # 永久静默该签名
hermes pause [--reason ...] # 全局紧急停止聊天里(或让 agent 自己调工具):
/cron add "in 30m" "Remind me to check the build"
/cron add "every 1h" "Summarize new feed items" --skill blogwatcher
/cron pause <job_id>
python
cronjob(action="create", skills=["blogwatcher", "maps"],
prompt="Look for new local events…", schedule="every 6h", name="Local brief")
cronjob(action="create", schedule="every 1d at 09:00", workdir="/home/me/projects/acme",
prompt="Audit open PRs, summarize CI health, and post to #eng")本机实测(2026-09-25 09:47–09:52):
$ hermes cron list → 3 个 active 任务
5deb2f224037 Time Log 周汇总 0 23 * * 0 Skills: personal-api
Workdir: /Users/fkycoya Deliver: origin
3a2f6d25b8d3 WebClipper 每日同步 0 23 * * * Skills: webclipper-feed-sync, lark-shared, lark-doc
e531a3a536e4 每日Session Log自动整理 0 17 * * *
$ hermes cron runs --limit 12 → 7 次尝试,全部 failed
RuntimeError: HTTP 402: Insufficient Balance (source=builtin)
$ hermes cron status
⚠ Gateway is running but the cron ticker looks STALLED — no heartbeat for 81889s
(expected every ~60s). PID: 37431
⚠ 1 job(s) last fired late: 5deb2f224037 catch-up after missed fire,
scheduled 2026-05-17T23:00:00+08:00, ran 2026-09-23T09:45:26 (128d 10h late)
$ hermes cron doctor
Cron doctor found 5 issue(s) across 3 job(s): …
我的收获
用户此前已学过本模块;以下是本机实测状态(2026-09-25):
- 本机 3 个任务全部
[active],配置完整且正好是官方「技能支撑的 cron 任务」一节的真实样本:5deb2f224037 Time Log 周汇总(0 23 * * 0,挂 skillpersonal-api,Workdir: /Users/fkycoya,Deliver: origin)|3a2f6d25b8d3 WebClipper 每日同步(0 23 * * *,挂三个 skillwebclipper-feed-sync, lark-shared, lark-doc)|e531a3a536e4 每日Session Log自动整理(0 17 * * *)—— 单技能与多技能两种形态都有实物。 - 执行台账
hermes cron runs实测 7 次尝试,全部failed,错误清一色RuntimeError: HTTP 402: Insufficient Balance(source=builtin,时段覆盖 09-23 09:45 补跑 → 09-23 17:09 → 09-23 23:00 → 09-24 17:00 → 09-24 23:00)→ 结论很明确:调度器一直在按计划工作,是 provider 余额没了(与你说的「DeepSeek 官方 API 停掉」对得上)。 - 🔴 但此刻 cron 实际是不转的(三条独立证据):①
hermes cron status报 ticker 心跳停摆 81889 秒 ≈ 22.8 小时(我实测ticker_heartbeat冻结在 09-24 11:03:49,等 8 秒后 mtime 不变)② 它报的 PID 37431 已不存在,当前网关是 37719 ③gateway_state.json写着"gateway_state": "startup_failed",exit_reason=api_server: API_SERVER_KEY was rejected by the startup guard (missing, placeholder/too short…),网关日志同款Refusing to start: API_SERVER_KEY is required+✗ api_server failed to connect,且gateway-starts.log尾部 6 条时间戳间隔只有 ~15–50 秒 → 网关正在重启循环(launchd 状态显示 78)。 hermes cron doctor实测 5 项发现跨 3 个任务,正好把官方列的检查项演了一遍:三个都有last run failed: HTTP 402;Time Log还带「上次是补跑:计划 2026-05-17 23:00、实际 2026-09-23 09:45:26,晚了 128 天 10 小时」;WebClipper还带「运行完成了但结果没投递(platform 'feishu' not configured/enabled)」。- ⭐ 最后那条是我 09-24 那次修复的直接后果:我按你的要求禁掉了本地 feishu(本地不再抢答销售群),而
cron任务的Deliver仍是origin→ 落在已禁用的 feishu 上 → 于是变成官方说的delivery_failed谱系(agent 干完活但没人收到)。这正好印证官方把「执行」与「投递」分开追踪的设计:只有分开记,才看得出是「没干活」还是「干完了没送到」。 hermes cron list里3a2f6d25b8d3显示(3 failures in a row)—— 官方failure_streak机制的实景;~/.hermes/cron/下三个.fire-*.lock的时间戳都停在 09-23 09:45,正是补跑那一刻留下的痕迹(.tick.lock则已是fkycoya属主、今天仍在更新,说明 4 月那次的 root 属主问题确实修好了)。- 💡 一条交叉印证:
Time Log 周汇总挂的技能是personal-api—— 而personal-api正是我先前排查出的路径失效技能之一(它引用的2-Areas/Identity/ME.md已在 09-23 重构中改名为15-个人档案/身份与能力/ME.md)。也就是说:就算 provider 余额恢复,这个任务仍然会因为技能取不到身份文件而做不成事 —— 官方给的hermes cron doctor(查技能就绪)与cron.preflight正是为了提前暴露这类问题。 - ⚠️ 记录一个我自己的操作坑:我第一遍跑
hermes cron doctor 2>&1 | head -25; echo $?拿到的退出码是head的而不是 doctor 的 —— 官方明确写 doctor「有任何发现时退出码 1」。管道会吞掉真实退出码,要判 doctor 的健康度必须直接跑或重定向到文件再取$?。
(个人感想部分待补写。)
待深入
(待填:没听懂、想回头查的。)