游乐游手机版
首页/AI教程/文章详情

Hermes Agent Bug修复全记录:诊断到CEO亲自加固

时间:2026-07-08 17:43
修复了HermesAgent两个关键Bug:一次性任务过去时间静默失败和同一任务因并发调度重复执行。CEO亲自审查,用持久运行声明(run_claim)替代了简单的60秒推进方案,避免了长任务重复触发。修复commit保留外部贡献者署名,体现了诊断能力比修复方案更核心的协作模式。

Hermes Agent 是 Nous Research 旗下的旗舰开源项目,但它的贡献规则有点特殊——你没法直接往主分支合并代码。从 2026 年 5 月起,项目进入了 salvage-only 模式:所有外部提交的 PR,都由维护者 cherry-pick 重写后再合入,commit 的作者默认是维护者自己。

向 Hermes Agent 提交 Bug 修复:从诊断到 CEO 亲自加固的完整记录

这意味着两件事:第一,你可以贡献代码,但除非维护者主动保留你的作者信息,否则在 git blame 里看不到你的名字。第二,你的方案会被 review,但不一定被采用——维护者很可能在你的思路上做一套完全不同的实现。

近 30 天的数据很说明问题:cron 调度子系统有 68 位外部开发者提交过代码,但只有 8 人获得了多个 commit 的作者署名。这篇文章记录的就是在这套规则下,两次让代码以作者身份合入主分支的经历——尤其是第二次提交时,CEO 亲自 review,并在我的方案上叠加了一套生产级的加固。

Bug #1:一次性任务为何“永远不触发”

现象

用户创建一个 schedule.kind = "once" 的 cron job,并指定 run_at 为未来某个时间点。但如果 run_at 不小心设成了过去的时间,会发生什么?直觉上应该报错,但实际上这个 job 会静默创建成功,然后永远不触发——它变成了一个“幽灵任务”,躺在 cron 数据文件里,调度器每次 tick 都跳过它,用户完全不知道发生了什么。

全链路追踪

问题的根源分布在三个入口。cron/jobs.py 中的 create_job() 函数是第一个入口,不过在我修复之前,它已经被另一个人(#59410)堵上了。但还有两个“后门”会绕过这个检查。

后门 1:update_job 的 fallback 路径

代码的本意是:如果 job 的 next_run_at 丢了(比如数据文件损坏),自动重新计算。但 compute_next_run() 对已经过去的一次性任务会返回 None——然后这个 None 被静默写入 next_run_at,调度器从此再也不会调度它。

后门 2:resume_job 路径

用户暂停了一个一次性任务,等想起来恢复时,run_at 已经过去了。resume_job 同样调用 compute_next_run(),得到 None,然后通过 update_job 写入——又是同一个静默失败。

修复

修复逻辑很直接,在两个后门处加入显式的过去时间检查:

# update_job fallback(修复后)
next_run = compute_next_run(updated["schedule"])
if next_run is None and updated["schedule"].get("kind") == "once":
    run_at = updated["schedule"].get("run_at", "unknown")
    raise ValueError(f"Requested one-shot time {run_at} is in the past "
                     f"(grace window: {ONESHOT_GRACE_SECONDS}s) and cannot be scheduled.")
updated["next_run_at"] = next_run

# resume_job(修复后)
next_run_at = compute_next_run(job["schedule"])
if next_run_at is None and job["schedule"].get("kind") == "once":
    run_at = job["schedule"].get("run_at", "unknown")
    raise ValueError(f"Cannot resume: one-shot time {run_at} is in the past "
                     f"(grace window: {ONESHOT_GRACE_SECONDS}s) and will never fire.")

ONESHOT_GRACE_SECONDS = 120 是一个宽容窗口——job 的 run_at 在当前时间之前 120 秒内仍然允许创建,防止时钟偏差导致的误拒绝。这个修复最终以 commit 8def4ccb4 合入主分支,author 保留为 isheng

Bug #2:同一个 job 为什么被执行了两次

问题来自真实用户

7 月 5 日,用户 gservat 提交了 issue #59229,报告了一个相当诡异的问题。日志清晰地复现了整个过程:

21:00:31  Running job 'Reminder' (ID: d9b7231ebaa4)
21:00:59  Running job 'Reminder' (ID: d9b7231ebaa4)  ← 同一个 job,28 秒后又启动了
...
21:03:06  delivered to telegram:718495351
21:03:29  delivered to telegram:718495351  ← 两条重复消息
21:03:29  WARNING: job_id d9b7231ebaa4 not found, skipping sa ve

根因分析

问题出在 _get_due_jobs_locked() 函数中。当你运行 hermes gateway runhermes serve(桌面版)时,两个进程各自有一个 60 秒间隔的 cron 调度器,它们操作同一份 cron 数据文件。

关键代码路径是这样的:调度器在 tick 循环中获取到期任务列表,然后逐个推进 next_run_at。但问题在于:advance_next_run() 只对重复执行的 job 有效。对于 one-shot job,调度器在拿到任务后不会修改 next_run_at。这意味着:

  1. 进程 A 的调度器在 21:00:31 tick,拿到 job,开始执行(这个 job 需要 2.5 分钟完成)
  2. 进程 B 的调度器在 21:00:59 tick,检查同一个 job——因为 next_run_at 没变,它仍然被认为是“到期”的
  3. 于是同一个 job 被执行了两次

进程内的 _running_job_ids 集合能防止同一进程重复调度,但它是一个内存结构——进程 B 看不到进程 A 的 _running_job_ids.tick.lock 文件锁只序列化 tick 的执行,但两个 tick 相隔 28 秒,锁早就释放了。

最初方案:+60s advance

思路是在 _get_due_jobs_locked() 中,对 one-shot job 在返回前将其 next_run_at 推进 60 秒(超过下一个 tick 窗口),并在文件锁持有期间立即持久化。这样进程 B 在下一次 tick 时,next_run_at 已经被推进了,不再是“到期”状态。mark_job_run() 在任务完成时会重新设置 next_run_at,所以即便调度器在 advance 之后崩溃,job 也只是延迟一个 tick 窗口。这个方案提交为 PR #59446。

CEO 的回应:你的方案不够

PR #59446 提交后,CEO @teknium1 没有直接 merge,而是在 #59524 中开了一个新的 salvage PR。他的评论翻译过来是:你的诊断完全正确,但 +60s 方案只是把问题推迟了一个 tick。如果一个任务运行超过 60 秒,它仍然会在下一个 tick 被重复触发。

CEO 的方案:durable run-claim

CEO 的方案不推进 next_run_at,而是引入了一个持久的运行声明(run_claim),镜像了代码库中已经存在的 fire_claim 模式:

  • 声明阶段——在 _get_due_jobs_locked() 中写入 run_claim,标记“这个 job 正在被哪个机器执行”
  • 检查阶段——在同样的函数顶部,扫描 due jobs 时,如果发现已有未过期的 run_claim,就跳过该 job
  • 清理阶段——在 mark_job_run() 中释放 run_claim

还有一个安全阀:ONESHOT_RUN_CLAIM_TTL_SECONDS = 1800(30 分钟)。如果持有声明的调度器进程崩溃了,30 分钟后声明自动过期,job 会被重新调度,不会永久卡住。

为什么 run_claim 比 +60s 更好

场景我的方案(+60s advance)CEO 方案(run_claim)
进程 B 在 28s 后 tick✅ 阻止✅ 阻止
进程 B 在 61s 后 tick(长任务)❌ 重新触发✅ 阻止
整个 2.5 分钟运行期间重复只被延迟到下一个 tick全程保护
调度器崩溃延迟一个 tick 恢复TTL 过期后恢复

核心差异在于:我的方案是时间窗口(“未来 60 秒内不要碰”),CEO 的方案是状态锁(“有我正在运行的声明就不要碰,直到我完成或崩溃”)。前者有竞态窗口,后者没有。

最终合入的 commit 链

这三个 commit 按顺序出现在 upstream/main 上:

  • 06cc983b8 — Author: isheng, Committer: Teknium。这是我原来的方案,CEO 亲自 cherry-pick,author 保留。
  • 8f849ea36 — Author: teknium1, Committer: Teknium。将我的 GitHub 账号注册到项目的作者映射表。
  • 3b5c64543 — Author: Teknium, Committer: Teknium。CEO 的改进:把我的 +60s 替换为 run_claim。

注意 06cc983b8 的 committer 是 Teknium,这意味着是 CEO 本人执行的 cherry-pick 操作。在 salvage 流程中,committer 代表谁做了代码审查和合并,author 代表谁写了原始代码。

复盘:几个值得记住的经验

第一,诊断能力比修复方案更值钱。 CEO 没有用我的代码,但他保留了 commit 署名,并公开说“your diagnosis was spot-on”。在 salvage 制度下,准确定位根因比写出完美修复更重要——维护者可以在你的诊断基础上做更好的实现,但诊断本身是你不可替代的贡献。

第二,时间窗口不是状态管理。 +60s 方案本质上是用“推迟问题”代替“解决问题”。60 秒是一个魔法数字,碰巧覆盖了 issue 报告中的 28 秒重复,但对于 2.5 分钟的长任务完全无效。run_claim 方案把问题建模为分布式锁而非时间窗口,这才是正确的抽象。

第三,在代码库中寻找先例。 CEO 的方案镜像了已有的 fire_claim 模式——这是代码库中已经存在的设计语言。如果提交前更仔细地读完 cron/jobs.py 中的 fire_claim 实现,也许自己就能想到这个方向。在大型项目中,最好的方案往往已经在代码库里了,只是还没被应用到当前问题上。

第四,salvage 不是拒绝,是协作。 在 salvage-only 时代,维护者重写你的代码不是否定你的能力——这是项目的质量门槛。06cc983b8(原始方案)和 3b5c64543(CEO 的加固)在 git 历史中相邻存在,完整记录了“外部贡献者诊断 → 维护者加固”的协作过程。这比一个直接的 merge commit 更有故事性。

附:如何验证这些 commit

git clone https://github.com/NousResearch/hermes-agent.git
cd hermes-agent
git log --author="isheng" --oneline
# 8def4ccb4 fix(cron): reject past one-shot timestamps...
# 06cc983b8 fix(cron): prevent double-execution of one-shot jobs...
来源:https://juejin.cn/post/7658839201942847526
上一篇ReAct如追女生,Plan-and-Execute如结婚 下一篇智能体元年与超级App抢滩对话入口的AI产业信号解读
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Claude Code官方教你Loop工程附6大省token技巧
AI教程 · 2026-07-08

Claude Code官方教你Loop工程附6大省token技巧

之前写过一篇《Loop Engineering 的保姆级教程》,从概念到多工具实战,比较全面地讲了循环工程的玩法。这两天 Claude Code 官方团队下场,发了一篇博客叫「Getting started with loops」,系统地整理了他们团队内部对「循环」的定义和分类。 这篇博客的含金量十

阿里云2核4G服务器价格与选型:实例规格、收费标准及活动价
AI教程 · 2026-07-08

阿里云2核4G服务器价格与选型:实例规格、收费标准及活动价

阿里云2核4G这个配置,可以说是个人站长和中小企业用户最常关注的“爆款”了。不过它的价格可不是一个固定的数字,而是跟实例规格、带宽、云盘类型、地域等等因素紧密相关。比如目前轻量应用服务器2核4G给到峰值200M带宽、50G ESSD云盘,抢购价能做到9 9元1个月或者199元1年。通用算力型u1实例

阿里巴巴研发效能实践日:敏捷精益项目管理报名
AI教程 · 2026-07-08

阿里巴巴研发效能实践日:敏捷精益项目管理报名

研发效能提升领域又有重磅消息了。阿里巴巴研发效能实践日——由阿里研发效能部主办的线下沙龙品牌,这次携手全球领先的项目管理协会PMI,共同聚焦“敏捷精益项目管理”这一核心主题。听起来就干货满满?别急,活动精心安排了4大主题演讲,旨在帮助参会者在思维层面实现突破,并且回去就能直接落地实践。更关键的是,参

RFID资产管理系统:企业资产数字化高效管控方案
AI教程 · 2026-07-08

RFID资产管理系统:企业资产数字化高效管控方案

数字化转型走到今天,传统人工管资产那套老办法——效率低、差错多、资产一挪窝就成“失踪人口”——已经越来越扛不住了。从仓库、车间到办公室,但凡资产流转量大、品类多的企业,都急需一套能实时盯、自动盘的方案。结合多行业的落地经验来看,RFID资产管理系统之所以能成为主流选择,核心在于它用射频技术把资产全生

智能体工作流知识沉淀:从一次修复到长期记忆
AI教程 · 2026-07-08

智能体工作流知识沉淀:从一次修复到长期记忆

好的,作为一位资深的技术专家和知识管理实践者,我将为你重新讲述这篇文章的核心内容,让这些观点和案例听起来更像是一次真诚的技术分享,而不是一份AI生成的报告。 在传统软件工程里,我们反复念叨“代码复用”,但到了AI Agent参与的工程时代,真正能产生复利的东西变了——从“代码复用”悄然转向了“知识复