这个节点还活着吗
「派出去的任务没有回音」这一类问题里,最先要回答的不是「哪里坏了」,而是 「它到底还能不能干活」。
这一页给一张按可信度排序的判据表。它不解释故障原因,只回答那一个问题, 而且只有最后一格能给出确定答案。
判据表
| 你看到的 | 强度 | 为什么 |
|---|---|---|
task 字段里的文字 | 无效 | 那是发送方写进去的(send_task 自己会写)。它显示什么,取决于最后一次谁发了什么,和节点状态无关。看到 session disconnected 之类的字样时,那不是状态字段。 |
status = idle | 弱 | idle 只表示「不忙」。它至少覆盖四种互不相同的现实(见下)。 |
心跳(last_seen_at)很新 | 弱 | 心跳由外层进程上报。外层活着、内层推理进程死了,心跳照样新。 |
send_task 返回 ok 而不是 alias_offline | 中等 | 说明路由通、消息已入队。它不说明有人会去处理。 |
status = offline | 中等(不是结论) | 比 idle 有信息,但不足以判定它死了。至少有一条路径会把一个活着的节点标成 offline,见下。 |
status = blocked | 无效(比 idle 还弱) | 一个 blocked 节点完全可能在几十秒内答完一条任务。blocked 说的是没有出口,不是「它停了」,见下。 |
degraded 字段 / app 里的「降级 · 原因」标签 | 强(只对「它干不了」这一方向) | 来自节点自己最近 10 分钟内的分层健康报告:App Server 握不上手、共存 TUI 不在、或模型登录失效。有它就基本确定这时派过去的活不会被执行(Hub 0.9.0-preview.86 起直接拒收,node_degraded)。没有它不说明健康:老节点、非 codex-app-server 节点、报告过期都不会带这个字段。见健康与降级。 |
| 它自己回了一句话 | 硬证据 | 这是唯一与观察者视角无关的信号。 |
结论:只有最后一格算数。 前四格全绿也可能是一个死节点;前四格全灰,它也可能只是 没人跟它说话。
status = offline 也不是结论
一个活着、已注册到 CommHub、SSE 已连的节点,anet node stop 可能打出
[anet] "<alias>" is not running locally (server notified offline)并且返回 0,而它的进程仍在运行。
机制:anet node stop 按 tmux 会话判断节点「在不在本地跑」。节点如果不是在 tmux 里起的 (例如用裸后台进程起),这条命令就看不见它,然后通知服务端把它标成 offline。
所以:Hub 上显示 offline 的节点,可能正在正常工作。
这种情况并不常见。写在这里是因为 offline 不构成「它死了」的证明: 和本页其余各格一样,结论仍然只能来自最后一格。
status = blocked 甚至不表示「它停住了」
名册显示 blocked、心跳几分钟前的节点,派一条任务照样可能在几十秒内回复。 长时间显示 blocked 的节点,也可能一直是活的;只凭这个字段判断「这条线停摆了」是错的。
机制:把 status 从 blocked 拉回来的路径只有 report_completion 一条 (server/src/tools.ts)。一个 agent 只要用 report_status 报过一次 blocked, 之后都靠 send_task / 终态 commhub_reply 干活,它就会一直显示 blocked。
也不要用「它 blocked 多久了」来补救:名册里没有「何时变成 blocked」这个字段。 updated_at 被心跳一直刷,一个长期 blocked 的节点,updated_at 可能只是几分钟前。 拿它当「已 blocked 时长」,会给一个卡了很久的节点印出「几分钟前」,比不显示更糟。
和本页其余各格一样,结论仍然只能来自最后一格。
status = idle 覆盖的四种现实
- 闲着:字面意思;
- 正在推理但没上报:节点 TUI 明确显示推理进行中(例如
Wandering… (1m 12s · ↓ 2.1k tokens)), 同一时刻anet node ls报的仍是idle; - 刚刚失败:agent-node 的错误路径在
finally里无条件报回idle, 所以一个刚 fail-closed 的节点对外宣称的也是idle; - 崩了:裸进程死亡时不会有人把状态改掉。
在实际运行中,心跳新鲜的节点的 status 往往全是 idle,很少看到 working。 一个实际上只有单一取值的状态字段,不携带判别信息。
怎么发那条探针
commhub_send_task(alias="<节点>", task="收到请只回一句 `git rev-parse --short HEAD` 的输出,不要开始任何工作")探针的内容必须是「只有真执行过才可能知道的东西」。
反例:让它回 OK / 收到 / 完成。这类回答在「真跑了」和「没跑但回了句话」两种现实下 取同一个值:它证明的是「有东西在替它回话」,不是「它执行了」。
正例(都要求节点在它自己那台机器上取值):
git rev-parse --short HEAD(当前仓的提交)hostname+ 当前工作目录- 某个只有它本地才有的文件的行数
并且在探针里写明「不要开始任何工作」,否则你在诊断的同时给了它一个任务, 后面就分不清它是在回你还是在干活。
顺序建议
anet node ls/anet info <alias>先拿到status与心跳,只用来排除 「它压根没注册」,不用来判活;- 发一条上面那种探针;
- 等一个回执。 等不到就当它不可用,并在记录里如实写「该节点本轮不可用」, 不要写「可能在忙」:那是把一个未知说成了一个具体状态。
这一页不回答什么
它不回答「为什么坏了」,也不提供自愈。节点级的自愈目前只有一块:codex-app-server 节点的 App Server 看门狗(agent-node 2.5.0-preview.95 起,app-server 死了或卡死时按原会话重新拉起,共存节点只在 Linux 上, 见健康与降级)。bridge 进程本身崩溃的自愈仍然没有内置机制, 相关讨论见 issue #534; 「失败后仍报 idle」那一格见 issue #811。
anet node start 的 stdout ✅ 也不算数
exit 0 + 打印 ✅ node "…" started detached (tmux session live) 不代表节点真起来。 早于 2.3.0-preview.40 的版本在 detached 场景可能假报。
真判据:tmux has-session -t "=<alias>" 返回 0。= 必须写,裸名字是前缀匹配,会在错误的会话上误报绿。
批量场景用 anet project up,它的退出码在 ≥ 2.3.0-preview.40 上可信。
节点卡在 stopping / starting,或操作没反应
下面说的自动恢复需要这两个版本
自动补偿恢复需要 @sleep2agi/agent-node ≥ 2.5.0-preview.49 和 @sleep2agi/commhub-server ≥ 0.9.0-preview.40。
用 anet -v 看你装的是哪一版。版本不够时下面说的「自动恢复」不会发生, 处理办法见本节最后一段。
症状
在 Dashboard 或 CLI 上停止 / 启动 / 删除一个 daemon 托管的节点之后:
- 节点一直停在
stopping或starting,不往下走; - 或者操作发出去之后什么都没发生,两边都不报错。
常见触发条件:操作发出的那一刻 daemon 正好不在线(daemon 重启、机器休眠、 网络抖动、SSE 断开重连的空窗)。
先做什么:等一次重连
hub 用一次性推送把请求送给 daemon。daemon 不在线时那次推送就没了, 所以你看到的不是「操作失败」,而是「操作好像没发生过」。
满足上面版本要求时,这件事会自己好:daemon 每次重新连上 hub,都会主动把 自己名下没做完的请求拉回来重放。
所以第一步是等它重连,而不是手动重启。 通常几秒到一分钟。 卡
starting的节点另有一道 60 秒的兜底,会把过期的中间态清掉,让你能重新下发。
怎么确认它真的恢复了
看 daemon 那台机器的日志(不是 hub 的、也不是 Dashboard):
tail -f ~/daemon-<name>.log重连之后应当能看到它把漏掉的请求捡回来执行。节点状态随之离开 stopping / starting。
别把 Dashboard 上的状态当唯一判据:这一类问题的特征恰恰是 两边都不报错,界面看起来一切正常。
还是不动,怎么办
按这个顺序: