# RFC-031：Agent 节点 SSE 重连韧性——瞬时断线永不放弃、消灭僵尸态

> 状态：**草案（Proposed）**（2026-07-16）
> 范围：`agent-node`（`src/cli.ts`）+ `agent-network`（`src/node-server.ts`）的 SSE 重连逻辑
> 定位：v0.11 可靠性收敛项（不是新功能，是把"永远在线"这条产品承诺补实）

## 1. 问题

一个 agent 节点与 hub 的 SSE 连接断开（hub 重启 / 维护窗口 / 网络分区）后，当前重连逻辑会在**连续断线满 1 小时后彻底放弃自动重连**，然后节点就再也收不到任何任务，只能人工 `anet node start` 手动恢复。

实测现象：某节点 00:15 报 `SSE error: fetch failed`，同刻判定"连续 >1h 连不上 hub，放弃自动重连"，之后进程一直挂着直到几小时后被人手动 Ctrl+C 才结束——期间它**活着但收不到活**。

对一支"永远在线的数字员工"军团来说，这意味着：**任何一次超过 1 小时的 hub 维护 / 重启 / 网络抖动，会让所有受影响节点永久失联，直到有人发现并逐个手动拉起。**

## 2. 根因（代码定位）

`agent-node/src/cli.ts` 的 `connectSSE()`：

- 重连用指数退避 `1s → 2s → 4s → 8s → … → 封顶 30s`（`BASE_DELAY_MS` / `MAX_DELAY_MS`），这部分没问题。
- 但有一个写死的 `ABANDON_AFTER_MS = 60 * 60 * 1000`（1 小时）：连续断线超过它，就 `error(...) ; return;`。
- `connectSSE()` 是 **fire-and-forget** 调用（无 `await`、无后续善后）。所以 `return` 之后**进程并不退出**——它被心跳等其它定时器撑着继续存活，但 SSE 已死：

  **= 僵尸态**：进程还在（监督工具 / 仪表盘可能还因心跳显示它"在线"），但它对新任务完全失聪。这是最坏的失败形态——"看着活着、其实装死"，比干脆崩掉更难被发现。

- 同一段"1 小时放弃"逻辑在两处重复：`agent-node/src/cli.ts`（连接失败分支 + 读循环结束分支，两处）与 `agent-network/src/node-server.ts`（同样两处）。四处口径要一起改，否则漂移。

## 3. 设计原则

**瞬时故障 = 永不放弃地重试；只有不可恢复故障才停——而且停就要"响亮地退出"，绝不留僵尸。**

一个节点以 30s 封顶重连 = 每分钟约 2 次请求，负载可忽略；hub 一旦恢复即自愈（重连成功后已有的 re-register 逻辑会立刻让它在仪表盘复活）。用"无限重试"换"永远在线"，代价极小、收益是产品核心承诺。

## 4. 方案（按优先级）

**① 瞬时故障永不放弃（核心）**
网络断 / hub 5xx / 连接被拒等瞬时错：**移除"1 小时放弃"**，一直按 30s 封顶重连下去。
- 可选降噪：连续断线超过某阈值（如 10 分钟）后，把退避封顶从 30s 放宽到 2–5 分钟，减少长维护窗口期的日志 / 网络噪音——**但永不停止**。

**② 只对不可恢复故障才停**
`401` 且按现有逻辑重载 `ntok_` 后仍失败（token 被撤销 / hub 库被重置）——这类才值得停。此时**不要**静默 `return`，而是走 ③。

**③ 消灭僵尸态**
任何"决定停止"的路径，一律 `process.exit(1)` 响亮退出，而不是裸 `return`。让 tmux / systemd / `anet project` 等监督层能观察到进程死亡并自动重启（携带最新 token）。**"死了被自动拉起"永远优于"活着但失聪"。**

**④ 断线要可见**
断线持续超过 N 分钟时，停发"健康"心跳或把状态标为 `degraded`，让仪表盘显示"重连中（已断 42 分钟）"而不是骗人的"在线"。让人能第一时间发现，而不是几小时后偶然撞见。

**⑤ 去重四处逻辑**
把 `cli.ts` 与 `node-server.ts` 里重复的重连 / 放弃逻辑抽成**一个共享重连 helper**，四处调用同一份，避免未来各自漂移。

**⑥（可选、更大）监督看门狗**
给 `anet project` 加一个轻量 watchdog：任何节点进程退出、或心跳变陈旧超时，就自动重启该节点。这是"永远在线"的硬保证。①②③ 单独已能解掉本 RFC 的问题；⑥ 是把可靠性再抬一档的独立增量。

## 5. 影响面 / 落地

- 改动文件：`agent-node/src/cli.ts`（`connectSSE`）、`agent-network/src/node-server.ts`；④ 涉及心跳 / 状态上报；⑥ 涉及 `agent-network/bin/cli.ts` 的 `anet project`。
- 行为变化：默认不再自动放弃重连——对绝大多数用户是纯正向（节点更"黏"）。唯一需要注意的是 401-不可恢复路径改为 exit(1)，要确保监督层存在时不会陷入 crash-loop（③ 的 exit 配合 watchdog 要有退避）。
- 测试：hub 停 >1h 后重启，节点应自动复活并 re-register；401 撤销 token 场景应 exit(1) 而非僵尸；长断线期间仪表盘应显示 degraded 而非 online。
- 兼容：无 schema / 协议变更，纯客户端行为，可随 agent-node / agent-network 补丁版发布。

## 6. 取舍

- **无限重试 vs 干净放弃**：选无限重试。放弃的原始意图大概是"别让僵尸节点永远敲一个已死的 hub"，但 30s 封顶下负载可忽略，而"放弃"带来的永久失联对本产品是不可接受的失败模式。
- **exit(1) vs 原地重试不可恢复错**：401-不可恢复选 exit(1)（配合监督重启带新 token）；没有监督层时可退化为"慢速重试 + 响亮告警"，但绝不僵尸。

## 7. 待定 / 需 owner 决策

- ①的降噪阈值与放宽后的封顶值（10min → 2min？5min？）。
- ③ exit(1) 与监督层的 crash-loop 退避策略（是否引入最小重启间隔）。
- ⑥ watchdog 是否纳入本 RFC 还是拆独立 RFC。
