DeepAI Paper Hermes Agent 教程 日志里写着 systemd,机器却是 macOS:Hermes shutdown forensics 被 launchd 带偏的细节

日志里写着 systemd,机器却是 macOS:Hermes shutdown forensics 被 launchd 带偏的细节

有些 bug 不会让服务崩溃,也不会让消息丢失,但它会把排障的人带到错误方向。

NousResearch/hermes-agent issue #25510 就是这种“小而毒”的问题:macOS 上 Gateway 由 launchd 管理,但 shutdown forensics 日志里却记录:

under_systemd=yes

看到这行日志的人,很容易顺着 systemd 的方向去查:unit、journal、restart policy、INVOCATION_ID、service timing……可机器明明是 macOS,PID 1 是 launchd,系统里根本没有 systemd。

这篇不讲大故障,而讲一个排障中很常见的陷阱:日志字段如果把运行环境标错,后面的所有判断都会跑偏。


问题现场:macOS 的 Gateway shutdown 日志像 Linux

issue 指向 gateway/shutdown_forensics.py 的一行判断:

ctx["under_systemd"] = bool(invocation_id) or ppid == 1

这段逻辑在 Linux 上看起来有点道理:

  • INVOCATION_ID 存在,通常说明进程由 systemd 启动;
  • 父进程 PID 是 1,也可能意味着服务由 init/systemd 接管。

但到了 macOS,这个假设就变了味。

在 macOS 上:

PID 1 = /sbin/launchd

所以 Gateway 由 launchd 启动时,ppid == 1 成立,但这不代表它在 systemd 下运行。

于是日志里出现了误导性的:

under_systemd=yes

这不是崩溃原因,但会污染排障判断

这个 issue 里有一个很重要的边界:它不会导致 Gateway 重启,也不是中断的直接原因。

它影响的是:

shutdown forensics 的上下文准确性。

换句话说,Gateway 可能真的发生了 shutdown;但日志在解释 shutdown 环境时,把 macOS launchd 标成了 systemd。

这会带来几个后果:

  • 排障者以为在查 Linux systemd 行为;
  • 误看 systemd restart / timeout / watchdog 方向;
  • 忽略 macOS launchd plist、KeepAlive、RunAtLoad 等真实管理入口;
  • 让跨平台 bug report 的上下文变得混乱。

日志不是事实本身,而是事实的摘要。摘要里写错系统管理器,后面就容易一层层误判。


为什么 ppid == 1 不能直接等于 systemd?

跨平台代码里最危险的一类判断是:

某个现象在 Linux 上常见
→ 就把它当成所有平台的语义

ppid == 1 就是典型例子。

在不同系统里,PID 1 可能是:

  • Linux: systemd / init / container init;
  • macOS: launchd;
  • 容器: tini / dumb-init / supervisord / 自定义 entrypoint;
  • Windows: 不是同一套 POSIX 语义。

所以 ppid == 1 只能说明“父进程是该平台的 1 号进程”,不能直接说明“under systemd”。

更准确的判断应该带上平台条件。


建议修复:Linux 上才用 ppid == 1 推断 systemd

issue 给出的修复很小,但很关键。

原逻辑:

ctx["under_systemd"] = bool(invocation_id) or ppid == 1

建议改成:

ctx["under_systemd"] = bool(invocation_id) or (
    sys.platform.startswith("linux") and ppid == 1
)

这个改法保留了两层判断:

1. 有 INVOCATION_ID,仍可认为是 systemd 相关; 2. 只有在 Linux 平台上,才把 ppid == 1 纳入 systemd 推断。

到了 macOS,ppid == 1 不再触发 under_systemd=yes

这样 shutdown forensics 里就不会把 launchd 伪装成 systemd。


macOS 上真正该看的是什么?

如果 Gateway 在 macOS 上由 launchd 管理,排查方向应该更接近这些:

~/Library/LaunchAgents/ai.hermes.gateway.plist

以及 launchd 相关字段:

  • Label
  • ProgramArguments
  • RunAtLoad
  • KeepAlive
  • StandardOutPath
  • StandardErrorPath
  • 用户级 LaunchAgent 是否加载

而不是 systemd 的:

  • unit file;
  • systemctl status
  • journalctl -u
  • systemd watchdog;
  • INVOCATION_ID timing alignment。

issue 里也提到:check_systemd_timing_alignment 已经会先检查 INVOCATION_ID,所以在 macOS 上返回 None 是合理的;真正错误的是 snapshot dict 里的 under_systemd 标志。


为什么这种“小日志 bug”值得单独写?

因为 Gateway 类项目的排障,很依赖上下文字段。

一个 shutdown 事件通常会被拆成多层:

谁启动了我?
谁给了信号?
是不是 supervisor 重启?
是不是平台 adapter 抛错?
是不是 Agent 自己 exit?
是不是系统服务管理器干预?

如果第一层“谁启动了我”就写错,后面分析会越来越偏。

尤其是用户把日志贴到 GitHub issue 或群里时,维护者第一眼看到 under_systemd=yes,会自然以 Linux/systemd 假设继续问问题。

但真实环境是:

macOS + launchd + ~/Library/LaunchAgents/ai.hermes.gateway.plist

这就是为什么 diagnostic accuracy 不是小事。


适合写进排障清单的一条规则

在 Hermes Gateway 这类跨平台组件里,可以把这条规则写进排障习惯:

看到日志里的 service manager 字段时,先和实际 OS / PID 1 / 安装方式交叉验证。

例如:

  • macOS 上看到 under_systemd=yes,先怀疑字段误判;
  • Windows 上看到 manual process,要确认是否其实是 Scheduled Task;
  • Docker 里看到 PID 1,也不要直接当成宿主 systemd;
  • Linux 上才继续深入 systemd unit / journal / watchdog。

这样可以避免把排障路线带进死胡同。


和模型调用链路的关系

Hermes 的完整运行链路通常很长:

消息平台 → Gateway → Agent → Provider / OpenAI-compatible API

shutdown_forensics 属于 Gateway 运行诊断层,用来帮助你判断服务是如何启动、如何退出、由谁管理。

当这层上下文准确后,才更容易判断问题是否继续向 Agent、工具调用、模型 Provider 或 API 网关方向推进。

如果你的部署里还接了 DeepAI API 中转站,建议把它放在模型调用层来观察:Base URL、API Key、模型 ID、响应延迟、错误码、用量统计。Gateway 的启动/退出上下文,则先看本机服务管理器和日志字段是否可信。

这种分层排查比一上来混查所有组件更省时间。


FAQ

macOS 上 PID 1 是什么?

通常是 /sbin/launchd,不是 systemd。

为什么 ppid == 1 会误判?

因为旧逻辑把 ppid == 1 当成 systemd 信号,但这个假设只适合部分 Linux 场景。macOS 的 PID 1 是 launchd。

这个 bug 会导致 Gateway 重启吗?

issue 明确说这不是 crash cause,也不会导致 Gateway restart 或 interruption。它影响的是 shutdown forensics 日志准确性。

macOS Gateway 应该看 systemctl 吗?

通常不应该。用户级 launchd 配置更可能在 ~/Library/LaunchAgents/ai.hermes.gateway.plist

修复方式是什么?

在判断 ppid == 1 时加上 Linux 平台限制,例如 sys.platform.startswith("linux") and ppid == 1


总结

#25510 的价值不在于它造成了多大的故障,而在于它提醒我们:

跨平台排障日志里的一个布尔值,也可能把人带错方向。

macOS 的 PID 1 是 launchd,不是 systemd。

所以 ppid == 1 不能直接推出:

under_systemd=yes

更稳的做法是让日志字段尊重平台语义:Linux 走 systemd 判断,macOS 走 launchd 语境,Windows 走 Scheduled Task / Service 语境。

排障时少一点错误暗示,就少走很多弯路。

Related Post

你以为传进 Docker 的变量,其实从没上车:Hermes docker_env 配置为何失效你以为传进 Docker 的变量,其实从没上车:Hermes docker_env 配置为何失效

Hermes 的 terminal.docker_env 写在 config.yaml 里却没有进入 Docker 容器,根因是 env_mappings 缺少 docker_env 到 TERMINAL_DOCKER_ENV 的映射。本文复盘 #20537:为什么配置看似存在,运行时却只拿到默认空 dict。

Gateway 明明活着,Hermes 却说 stopped:Windows Scheduled Task 状态误判的排查Gateway 明明活着,Hermes 却说 stopped:Windows Scheduled Task 状态误判的排查

Windows 上 Hermes Gateway 通过 Scheduled Task 正在运行,Telegram 也在线,但 hermes gateway status 却显示 stopped 或 manual process?本文复盘 #25513:runtime snapshot 缺少 Windows 分支,PID 扫描又把真实 gateway run 进程漏掉。