Hermes Agent 实战 10|升级不翻车,与给上游提 PR:一个被冲掉三次的修复原创
# 升级不翻车,与给上游提 PR:一个被冲掉三次的修复
系列第 10 篇。Hermes 是活跃开源项目,频繁更新。但我的
main上有本地独有的补丁——最关键的一个是跨 Profile SSH 泄漏修复。这篇讲怎么合并上游又不丢本地补丁、怎么验证补丁还活着、以及那个被上游合并冲掉了三次的修复的完整复盘,最后讲怎么把它反哺成 PR。
# 1. 升级的标准动作
cd ~/.hermes/hermes-agent
git merge origin/main # 合并上游
uv sync --all-extras # 同步依赖(注意:必须 --all-extras)
scripts/run_tests.sh # 跑测试
2
3
4
两个本地特有的坑先说:
- 依赖必须
uv sync --all-extras,不能是裸uv sync。裸uv sync会把环境裁剪到「锁文件 + 默认 extra」,删掉所有可选 extra(各类 IM 平台 SDK、数据库驱动……),让一堆 Profile 静默失效。 - 改完任何源码/依赖都要重启网关——Python 在 import 时缓存模块,不重启等于没改。
# 2. 本地补丁会被上游「静默冲掉」
main 领先 origin/main 若干本地提交。合并上游时,上游对同一片代码的重写会悄无声息地盖掉你的修复——不报冲突,因为它整段重写了那个函数。
我那个跨 Profile SSH 泄漏修复(核心是让 _resolve_container_task_id() 按会话返回 session:<key> 而不是 "default"),就这样被冲掉了三次:
| 时间 | 事件 |
|---|---|
| 原始修复 | 把容器 task_id 按会话隔离 |
| 第一次合并 | 上游重写了这个函数 → 修复静默丢失 |
| 重新应用 | 以兼容上游新设计的方式重打 |
| 第二次合并 | 本地 main 被快进到一个上游 commit,本地提交整个不在祖先链里了 |
| 再次重应用 | 打在上游新的函数体上,并补回被上游删掉的回归测试 |
教训:「合并没冲突」不代表「你的补丁还在」。上游重写 + 快进,都能让你的修复人间蒸发,而 Git 不会警告你。
# 3. 每次升级后,验证补丁还活着
光记得「我打过补丁」没用,得有可执行的验证步骤。我给这个修复配了固定的 4 步自检:
# 1. 修复点还在该在的函数里
grep -n "HERMES_SESSION_KEY" tools/terminal_tool.py
# 必须出现在 _resolve_container_task_id 内(别和 sudo 密码缓存那个同名修复搞混)
# 2. 该函数没有退化成无条件 return "default"
# 3. 回归测试必须过
scripts/run_tests.sh tests/tools/test_shared_container_task_id.py
# 4. 重启 webui + 网关(Python 模块缓存)
2
3
4
5
6
7
8
9
可复现的通法:任何「本地领先于上游」的关键补丁,都要做三件事——
- 把它保留成一个带 tag 的 commit(丢了能从 tag 找回);
- 写一段能跑的验证(grep + 测试),而不是靠记忆;
- 每次合并后立刻跑这段验证,把「补丁还在吗」变成一条命令而不是一次祈祷。
# 4. 已知会失败、但不是回归的测试
升级后跑测试,要能区分「真回归」和「本机环境本来就过不了的」。我维护了一张已知良性失败清单(缺某个可选依赖、home 路径特殊、host ripgrep 行为差异等),写在项目的 CLAUDE.md 里。没有这张清单,你每次升级都会被一堆红色吓到,然后逐渐麻木到漏掉真回归。这张清单本身就是升级流程的一部分。
# 5. 把修复反哺给上游
本地补丁终究是负担——每次升级都要盯。正解是让上游合并它,负担就归零了。我把 SSH 泄漏修复提了 PR,也踩了开源协作的现实:
有价值的 PR 提完,要在评论里 @ 一下核心维护者。
主合并人几乎只合自己和核心团队的 PR。社区 PR——哪怕被打了高优先级标签——不主动 ping 就石沉大海。我那个 P1 的 SSH 修复挂着标签躺了好几天没人看,直到我去评论区 @ 维护者解释这个 bug 才有动静。triage 机器人贴标签很快,但贴标签 ≠ 有维护者会看到。
还有一条我自己的规矩:提交给开源项目的 commit,不要带 AI 的 Co-Authored-By 签名——有些维护者介意 AI 协作者,会因此拒掉 PR。
# 6. 可复现 checklist
- 升级三件套:
git merge origin/main→uv sync --all-extras→ 跑测试 → 重启网关。 - 本地关键补丁:保留成带 tag 的 commit + 写可执行验证 + 每次合并后立刻验。
- 「无冲突」别松懈——上游重写/快进会静默吞掉补丁。
- 维护一张已知良性失败清单,把真回归从噪声里捞出来。
- 能上游就上游:提 PR 消灭本地负担;提完 @ 维护者;commit 别带 AI 署名。
下一篇是踩坑合集——那些不够单独成篇、但每一个都真实坑过我的小事:checkpoint 把磁盘吃满、Profile 目录莫名其妙多出空文件夹、以及为什么「手动 rm」从来不是真正的修复。
# 6. 升级后的完整验证流程
把"补丁还在吗"变成一条命令而不是一次祈祷——这是我每次合并上游都跑的 6 步:
# 1. 确认本地干净、当前在 main
cd ~/.hermes/hermes-agent
git status --p<PASSWORD> | wc -l # 必须 0
git branch --show-current # 必须 main
# 2. 拉上游 + 合并
git fetch origin
git merge origin/main
git log --oneline -5
# 3. 同步依赖(必须 --all-extras)
uv sync --all-extras
# 4. 跑测试(区分真回归 vs 已知失败)
scripts/run_tests.sh 2>&1 | tee /tmp/test.log
# 5. 验证本地补丁还活着(以 SSH 泄漏修复为例)
grep -n "HERMES_SESSION_KEY" tools/terminal_tool.py
scripts/run_tests.sh tests/tools/test_shared_container_task_id.py
# 6. 重启服务(Python 模块缓存)
systemctl --user restart 'hermes-gateway-*'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 6.1 当 git merge 真的冲突了
git status
git mergetool
git add <file>
git merge --continue
# 再次跑第 5 步验证本地补丁还在
2
3
4
5
# 6.2 测试失败——区分"真回归"还是"本机环境"
| 失败模式 | 是回归吗 | 怎么办 |
|---|---|---|
tests/acp/* 全部 | ❌ 已知(缺 acp 可选依赖) | 跳过 |
test_wecom_callback | ❌ 已知(缺 defusedxml) | 跳过 |
test_resolve_path / test_file_sync_back | ❌ 已知(home 路径特殊) | 跳过 |
test_search_error_guard.py::test_files_only_excludes_diagnostics[_search_with_rg] | ❌ 已知(host ripgrep 行为差异) | 跳过 |
| 其他任何失败 | ✅ 极可能是回归 | 回滚 git merge,先解决再继续 |
# 7. Agent 可直接解析的元数据块
{
"_meta": {
"doc_version": "2025-01-15",
"article_id": "hermes-10-upgrade-pr",
"profile_context": "dev",
"executable_examples": true,
"dependencies": ["git", "uv", "pytest"],
"estimated_setup_time": "60min"
},
"quick_start": {
"prepare_branch": "git checkout main && git pull origin main",
"merge_upstream": "git fetch origin && git merge origin/main",
"sync_deps": "uv sync --all-extras",
"run_tests": "scripts/run_tests.sh 2>&1 | tee /tmp/test.log",
"restart_services": "systemctl --user restart 'hermes-gateway-*'"
},
"test_matrix": {
"full_suite": "scripts/run_tests.sh",
"single_file": "scripts/run_tests.sh tests/path/to/test.py",
"with_args": "scripts/run_tests.sh tests/foo.py -- --tb=long -v",
"excluded": ["tests/acp/*", "test_wecom_callback", "test_resolve_path"]
},
"known_failures_ignore": [
"tests/acp/* (optional acp dep absent)",
"test_wecom_callback (defusedxml absent)",
"test_resolve_path (home resolves to /data/<USERNAME>/home, no SSH backend)",
"test_file_sync_back (home resolves to /data/<USERNAME>/home, no SSH backend)",
"test_search_error_guard.py::test_files_only_excludes_diagnostics[_search_with_rg]"
],
"safety_rules": [
"升级前必须在 main 分支且干净 (git status --p<PASSWORD> | wc -l == 0)",
"uv sync --all-extras 后必须跑 tests",
"改源码必须重启 gateway 才生效",
"永远不要 force push 到 main",
"本地关键补丁升级后必须 grep 验证还在"
],
"verification": {
"git_clean": "git status --p<PASSWORD> | wc -l | xargs test 0 -eq",
"on_main": "git branch --show-current | grep -q '^main$'",
"uv_lock_fresh": "test uv.lock -nt pyproject.toml",
"tests_pass": "grep -E 'passed|failed' /tmp/test.log | tail -5",
"gateways_up": "systemctl --user is-active hermes-gateway-default.service",
"local_patch_alive": "grep -q 'HERMES_SESSION_KEY' tools/terminal_tool.py"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
AI Agent 使用指南:
- 当用户说"我要升级 hermes" → 按顺序执行
quick_start.prepare_branch→quick_start.merge_upstream→quick_start.sync_deps→quick_start.run_tests - 当用户说"检查升级前状态" → 依次执行
verification.git_clean,verification.on_main,verification.uv_lock_fresh - 当用户说"测试失败怎么办" → 对照
known_failures_ignore列表,如果是已知失败则跳过,否则检查依赖或回滚 - 当用户说"我的本地补丁没了" → 执行
verification.local_patch_alive验证,若失败用git log --all --oneline | grep <keyword>找回带 tag 的 commit - 当用户说"重启服务" → 执行
quick_start.restart_services