Hermes gateway 鲁棒性永久修复方案
1. 临时止血:外层 watchdog
当前先不改 Hermes 源码,用 PowerShell watchdog 把 gateway 进程守住:
powershell -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\hermes\run-gateway-watchdog.ps1"脚本位置:
C:\Users\yunlu\AppData\Local\hermes\run-gateway-watchdog.ps1日志位置:
C:\Users\yunlu\AppData\Local\hermes\logs\gateway-watchdog.log行为:
- 循环运行
hermes gateway。 - gateway 退出后记录退出码和运行时长。
- 按
10s -> 20s -> 40s -> ... -> 300s退避重启。 - 如果 gateway 稳定运行超过 300 秒后才退出,下一轮退避恢复为 10 秒。
- 不注册开机自启,不改 Hermes 源码,不改 git 仓库。
这个方案只能解决“进程死了自动拉起”,不能解决 Discord zombie websocket、状态不可信、DeepSeek turn 失败补偿等根因。
2. 永久目标
把 Hermes gateway 改成常驻 supervisor:
gateway 主进程常驻
├─ Discord adapter 断线:标记 discord=retrying,后台指数退避重连
├─ DeepSeek/provider 断流:当前 turn failed/retryable,记录状态,不杀 gateway
├─ cron/background review 报错:记录 job failed,不影响主消息通道
└─ health/status 真实反映:running / degraded / retrying / failed核心原则:
- 平台连接失败不等于 gateway 失败。
- 模型调用失败不等于 gateway 失败。
- 子任务失败不等于主进程失败。
3. 需要修改的核心文件
gateway/platforms/discord.py
目标:Discord 网络错误可恢复,重连前必须关闭旧 client。
要点:
- 在
connect()创建新的commands.Bot前,如果self._client存在且未关闭,先await self._client.close()。 - 同步清理
self._bot_task和self._ready_event,避免旧 websocket 残留和重复响应。 - 启动 bot 时使用
self._client.start(..., reconnect=False),让 gateway runner 统一管理重连策略。 - READY 等待超时应标记 retryable failure,而不是悬挂。
- 网络类错误标记为 retryable:
aiohttp.ClientConnectorErroraiohttp.ServerDisconnectedErroraiohttp.ClientOSErrorasyncio.TimeoutErrorConnectionResetErrorOSErrordiscord.ConnectionClosed
- 配置/权限类错误标记为 failed:
- token 缺失
discord.LoginFailurediscord.PrivilegedIntentsRequired- 明确的权限/配置错误
gateway/run.py
目标:retryable 平台错误不让 gateway 退出。
要点:
GatewayRunner.start()中,如果所有启用消息平台启动时都只是 retryable 网络失败:- 返回
True。 - 保持 gateway 进程运行。
- 写
gateway_state="degraded"。 - 把失败平台放进
_failed_platforms等后台重连。
- 返回
_handle_adapter_fatal_error()中,如果 runtime retryable 断线:- 从
self.adapters移除该 adapter。 - 写
platforms.discord.state="retrying"。 - 放入
_failed_platforms。 - 即使最后一个消息平台断开,也不要
await self.stop()。
- 从
- non-retryable 错误才标记
failed,并按现有逻辑停止该平台或请求干净退出。 _platform_reconnect_watcher()退避策略改为:- 初始 5 秒。
- 指数退避。
- 最大 300 秒。
- 加 jitter。
- retryable 网络错误不因为固定 20 次失败就永久放弃。
gateway/status.py
目标:gateway_state.json 能真实表达 degraded/retrying/provider 状态。
建议结构:
{
"gateway_state": "degraded",
"platforms": {
"discord": {
"state": "retrying",
"error_code": "discord_client_error",
"error_message": "Cannot connect to host gateway-us-east1-d.discord.gg",
"next_retry_at": "2026-05-15T18:31:45+00:00",
"updated_at": "2026-05-15T18:31:15+00:00"
}
},
"providers": {
"deepseek": {
"status": "retrying",
"model": "deepseek-v4-flash",
"base_url": "https://api.deepseek.com/v1",
"session_id": "20260515_095752_5e4d8d3b",
"message_id": "discord:1496306840106045451",
"attempts": 3,
"error": "peer closed connection without sending complete message body (incomplete chunked read)",
"retryable": true,
"next_retry_at": "2026-05-15T19:24:03+00:00",
"updated_at": "2026-05-15T19:19:03+00:00"
}
}
}状态枚举:
- Gateway:
starting / running / degraded / draining / stopped / startup_failed - Discord:
starting / connected / retrying / disconnected / failed - DeepSeek/provider:
ok / degraded / retrying / failed
Provider turn/status 相关路径
候选位置:
run_agent.pygateway/run.py的GatewayRunner._run_agent(...)- background review / auxiliary agent 调用处
目标:
- DeepSeek
RemoteProtocolError、incomplete chunked read、APIConnectionError、timeout 等网络错误重试耗尽后,只让当前 turn 失败。 - 返回结果中带上:
failed=Trueretryable=Trueprovidermodelbase_urlsession_idmessage_idattemptserrornext_retry_at
- 写入
gateway_state.json.providers.deepseek。 - 如果消息 adapter 仍可用,给用户发送简短提示:
模型连接中断,本次任务已记录,可稍后重试。- 不要继续向上
raise到 gateway 主循环。
4. 测试用例
建议新增或更新这些测试:
tests/gateway/test_discord_connect.py
tests/gateway/test_runner_startup_failures.py
tests/gateway/test_platform_reconnect.py
tests/gateway/test_runner_fatal_adapter.py
tests/gateway/test_status.py必须覆盖:
- Discord connect 抛
aiohttp.ClientConnectorError:- gateway 返回启动成功。
gateway_state="degraded"。platforms.discord.state="retrying"。- 进入后台重连队列。
- Discord runtime bot task 因网络错误结束:
- 不调用
runner.stop()。 - 不写 fatal exit。
- 进入后台重连队列。
- 不调用
- Discord token/auth 错误:
- 标记
failed。 - 不无限 retry。
- 标记
- Discord 重连成功:
platforms.discord.state="connected"。- 如果没有其他 degraded 项,
gateway_state="running"。
- DeepSeek stream retry 耗尽:
- 当前 turn
failed=True。 - provider 状态写为
retrying或degraded。 - gateway 继续运行。
- 当前 turn
- Provider 后续成功:
providers.deepseek.status="ok"。
建议验收命令:
python -m pytest tests/gateway/test_discord_connect.py -q -n0
python -m pytest tests/gateway/test_runner_startup_failures.py tests/gateway/test_platform_reconnect.py tests/gateway/test_runner_fatal_adapter.py -q -n0
python -m pytest tests/gateway/test_status.py -q -n05. 验收标准
- Discord 代理/网络断线时,日志不再出现把网络错误当成 gateway fatal exit 的行为。
gateway_state.json能看到 Discordretrying,整体gateway_state为degraded。- Discord 恢复后自动重连,状态回到
connected/running。 - DeepSeek 断流重试耗尽后,用户收到当前任务失败提示,gateway 仍继续接受后续消息。
- background review / cron 报错只记录失败,不影响主消息通道。
- 不新增第三方依赖,不大改架构。
6. 备注
当前本机日志已经观察到相关症状:
Discord client stopped: Cannot connect to host gateway-us-east1-d.discord.gg
Fatal discord adapter error (discord_client_error)
RemoteProtocolError: peer closed connection without sending complete message body (incomplete chunked read)
Stream exhausted on attempt 3/3这些错误都应被视为可恢复条件,而不是 gateway 主进程失败条件。
2026-10-09 Codex 审阅说明
这是 2026-05-15 的 Windows 修复方案,不构成“永久修复”或当前 CachyOS 服务验收。pytest -n 0 的 -n 属于 pytest-xdist 插件;没有该依赖时用普通 pytest,不能把参数报错判作测试失败。