Files
dsh_ai1net_server/归档/交接单-20260924-归档/交接单_presence在线态_20260917.md
T
admin 7bd1151c67 chore(工作区): 归档交接单 27 件 + 新增 CODEBUDDY §9 工作区卫生
交接单按归属约定(正文落文档库、工作区只放指针)归档至
归档/交接单-20260924-归档/,含逐件判定 README:
- 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26)
- 4 件主题已被覆盖网络线入口汇总
- 7 件历史接续包/规划件

CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、
工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
2026-09-24 07:58:57 +08:00

766 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交接单 · presence(在线状态)改造
> **序 ⑱(规划棒)产物** | 2026-09-17 | 状态:**待执行**(= 下一棒 序 ⑲ 执行棒)
> **性质**:**只出规划** —— ⛔ 本棒未改任何代码、⛔ 未动 47/106 配置、⛔ 未 commit / push、⛔ 未改任何生产值。
> **依据**:`接续入口_覆盖网络线_20260916.md` §2「🎯 本轮动作」|`覆盖网络_应用场景与待完善清单_20260916.md` §五 第 7 步 + §二 / §四|`覆盖网络_瓶颈落地方案_20260916.md` **§1** + 总览|`覆盖网络_千台全场景推演_20260916.md` 瓶颈排序 / 流量预算总表
> **用户拍板** = 「**a 要做**」(2026-09-17 17:1x)⇒ presence 先做。⚠️ **B(内容分发 / 块级内容寻址)仍待拍板 ⇒ 本单只登记、⛔ 不规划**(见 §3.3-1)。
> **口径**:本单 **§8 前缀指纹** = `sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum`(⛔ 不是全文件 md5)。
---
## §0 摘要
**一句话**:presence 的第一刀必须落在**节点 / 端点在线态**(overlay node presence)上 —— 因为**全仓零应用层**(`room|chat` 命中 **0**,`presence` 字样**只出现在一处注释里**),而"房间层 presence"(1000 人大房那套订阅裁剪 / 抽样降频)**没有载体、无从下手**;节点层是它的**真前置**,且**今天就落在真实代码面上**:Manager 现在的在线态来源是**拉 relay `/status` 快照 + 15 s 陈旧回退**,这正是要替换的那条"轮询"路径。
**做法**(照抄 `瓶颈落地方案 §1` 的七条,逐条映射到本仓):
①presence 绑**连接生命周期**(WS 连上 = 在线、断开 = 离线)|②**本地批合并**(固定 1 s 窗口)|③**订阅式扇出**(只推给订阅了该 host 的 Manager / Worker)|④**批量事件**(一帧带 host 数组)|⑤**grace 10 s + 离线 debounce 30 s**|⑥**同 hostId 多连接按 device 聚合**|⑦**最终一致 5–15 s**(⛔ 不上强一致),外加 **TTL 安全网**(防 relay 崩溃漏事件)。
### §0.1 复核证据(本棒实测,3 条命令族)
| # | 命题 | 命令 | 实测输出 | 判定 |
|---|---|---|---|---|
| P1 | 全仓**零应用层**(房间 / 对话) | `grep -rilE "\b(room|chat)\b" src \| wc -l` | **0** | ✅ 成立 |
| P2 | `presence` 字样**只在注释里** | `grep -ril presence src` | **仅 `src/net/relay/placement.ts`**;该文件 **第 21 行**原文:`而我们的第一瓶颈是 **presence(在线态)**,所以速度略重(传输方案 §12 的实测依据)` | ✅ 成立(**零实现**) |
| P3 | 现役在线态来源 = **拉快照** | `grep -n "req.url === '/status'" src/net/relay/server.ts` ⇒ **413**;`grep -n "为什么必须真的把条目从 endpoints 里删掉" src/net/relay/server.ts` ⇒ **1234** | 1234 行原文写明:**「`/status` 是 Manager …」** ⇒ Manager 消费的是 relay 的 `/status` 快照 | ✅ 成立(**即"轮询"反模式**) |
> `src/net/relay/` 现存 15 个模块:`addr-override / client / dialer / directory / duplex / identity / index / keys / main / network / placement / rendezvous / server / switcher / wire` ⇒ **presence 的落点就是这一层**(不是新起一层)。
### §0.2 与入口旧结论的对账
- 入口 §2 曾记(2026-09-16):「仓库里目前**没有任何应用层代码** ⇒ "presence 改造"**无从下手**,这条旧指示是上一轮的笔误」。**本棒的处置**:该结论**前提仍然成立**(P1/P2 实测),但**结论要收窄** —— 不是"presence 无从下手",而是"**应用层 presence 无从下手**、**节点 presence 正当时**"(序 ①–⑰ 已把它的全部前置做完:网抽象+引导 ②、一机一钥+信任根 ③、443 兜底 ④、参数表+观测 ⑤、3–5 台最小形态 ⑥、失败切流+冷却语义 ⑦⑧、检测时延 ⑨)。
- 入口 §2 另有:「⛔ 未为 presence / 房间层 / 内容分发**定序**(只登记候选与优缺点)」+ 序 ⑰ 执行棒的「登记门禁」。⇒ **本棒已拿到拍板**(用户「a 要做」),门禁解除,**presence 已由"候选"转为"在册执行项"**。
### §0.3 步骤定序(理由)
**S0 基线 → S1 机制(relay 侧表)→ S2 线协议帧 → S3 批合并 → S4 消费侧 → S5 观测 → S6 真机验收 → S7 收口**。
- S1 必须最先落:**机制是唯一真活**,S2/S3 都只是它的出口形式;先红后绿在 S1 就能建起来。
- S2 与 S3 **同一批改**(帧与批合并同属"出口形式",拆开要跑两遍 build / scp)。
- S4 消费侧放 S3 之后:**只有出口定了才谈接线**,否则接线要返工(序⑭ 的教训:消费者先动 = 方向反)。
- S5 观测紧贴 S6:**没有阈值就不许跑真机**(否则"降幅多少算过"无法判定,= 序⑤ 的立单理由)。
- S6 是唯一动服务的一步,**置末**;S7 收口含部署(**属 lane 内执行细节,直接做、⛔ 不上抛**)。
### §0.4 范围外登记(⛔ 本单不规划)
| # | 项 | 为什么不在本单 |
|---|---|---|
| 1 | **B 内容分发 / 块级内容寻址** | **仍待用户拍板**(复杂度已评估为「中」:块级切分与哈希标识 + 内容寻址存储与客户端校验 + 同网段 peer 发现 + 内容源优先级链;验收判据单值可测「回源字节数 ≈ 1 份 × 组数」)⇒ **只登记一行**,⛔ 不写步骤、不定序、不留键位 |
| 2 | **房间层 presence 专属做法** | 百分比抽样降频 / idle / typing / 1000 人大房批量 bulk:**应用层专属**,且应用层不存在(P1)⇒ 只登记;⛔ 参数表**不留空键位**(口径要可执行) |
| 3 | 游戏服放 L1(三件事之二) | 属"部署规范 + 容量",与 presence 无耦合 ⇒ 另立 |
| 4 | `src/worker/tunnel.ts`(SSH 隧道,**生产死路径**)是否删 | 序 ⑯ 已判**独立决策** ⇒ 本单不处置 |
---
## §1 目标(一句话,可判定"做完了没有")
> 把覆盖网络的**节点 / 端点在线态**从「Manager 拉 relay `/status` 快照 + 15 s 陈旧回退」改为「**连接生命周期驱动 + 订阅式推送 + 本地批合并 + TTL 安全网**」;**完成判据 = 稳态 presence 帧数为 0、一次上下线变化 ≤1 帧、相对旧路径的帧数降幅 ≥ 1 个数量级(本机多实例实测 N≥4)、`/status` 兜底仍可用、且 `RELAY_FAILOVER_*` / `HB_SEC` / burst 一字未动(D1 自证)**。
---
## §2 只读前置(执行前**必须**先核实的 5 条;给命令与期望输出,⛔ 不许靠推断)
| # | 要核实的事实 | 命令(cwd = 代码仓 `D:/github/dsh_shenxian`) | 期望输出 |
|---|---|---|---|
| **P1** | 全仓**零应用层** ⇒ 本单只做节点 presence | `grep -rilE "\b(room|chat)\b" src \| wc -l` | **0**(≠0 ⇒ 先用 `head` 看是谁,再决定是否调整 §3 范围) |
| **P2** | presence **零实现** | `grep -ril presence src` ;`sed -n '21p' src/net/relay/placement.ts` | 仅 `placement.ts`;第 21 行 = **注释**(含「第一瓶颈是 presence」) |
| **P3** | 现役在线态 = **拉快照**(要替换的那条路) | `grep -n "req.url === '/status'" src/net/relay/server.ts` | `413`(行号以实测为准,⛔ 别写死) |
| **P4** | **D1 基线快照**(改前拍一次,收口时逐项比对) | 参数表 `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` ;并在参数表内 `grep -n "RELAY_FAILOVER_\|HB_SEC\|GRACEFUL_BURST"` | 参数表指纹(**序⑰ 收口值 = `8f08e74b026e6e5b5e1b3db813f031ae`**,取数时间 2026-09-17 17:2x)+ 6 个 `RELAY_FAILOVER_*` 与 `HB_SEC` / burst 现值逐行留档 |
| **P5** | 收口基线(零回归三件套的**改前值**) | 代码仓 `npm test`(**Node 22**:`npm.cmd test`)/ `node scripts/overlay-failover-drill.cjs --scene all --table "<工作区根>/参数表_覆盖网络_20260917.md"` / `node scripts/overlay-probe.cjs --table "<工作区根>/参数表_覆盖网络_20260917.md"` | `npm test` **162/161/0/1**、`--scene all` **12 PASS / 0 SKIP / 0 FAIL**、`overlay-probe` **12/12**(取值以实测为准,**必须带时间戳**) |
> ⚠️ P4 / P5 的数字**必须带"取数时间 + 复核命令"**,⛔ 不写死绝对值(`交接单/README.md §二` 的坑 2:写死的数字会被并行改动打穿)。
> ⚠️ 对 47 / 106 的任何命令:**ssh 一律显式 `-p 22`**(别名 `bt-server` 的 `32022` 是**失效残留口**);取证输出一律 `2>/dev/null`(PQ 告警会污染计数判据)。
---
## §3 范围
### 3.1 要改的(预计 8 个文件;**以实测为准**,多一个都要按 §9-3 停下报告)
| 层 | 文件 | 改什么 |
|---|---|---|
| 机制(主体) | `src/net/relay/server.ts` | presence 权威表(`hostId → {state, connId, deviceCount, lastSeenMs, endpoints[]}`)+ 订阅表 + 推送出口 + **TTL 安全网** |
| 机制 | `src/net/relay/wire.ts` | 新增帧:`SUB` / `UNSUB` / `PRESENCE` / `SNAP`(**帧号在既有分配表末尾追加**,⛔ 不改既有帧号语义) |
| 出口 | `src/net/relay/dialer.ts` 或 `client.ts` | 批合并调度(1 s 窗口)落点(S3 时定,二选一,⛔ 不两处都改) |
| 消费 | `src/net/relay/rendezvous.ts`(`RelayRendezvous`) | 订阅为主路径、`/status` **降级为兜底 / 对账**(⛔ 不删) |
| 消费 | `src/web/server.ts` | **C1 装配点**:`refreshOverlay` 一带改为订阅驱动(去掉"只能靠拉"的假设) |
| 消费 | `src/worker/relay-tunnel.ts` / `src/worker/agent.ts` | **C2 装配点**:同族接线(若实测无需改 ⇒ 在 §8 写明"已核对、无需改",⛔ 别为对称而改) |
| 测试 | `test/relay.test.mjs`(+可选新增 `test/relay-presence.test.mjs`) | 先红后绿用例;⛔ **不许改 `package.json` 的测试入口语义** |
| 观测 | `参数表_覆盖网络_20260917.md` + `scripts/overlay-probe.cjs` | 新增 `PRESENCE_*` 键(**全部有实测值,⛔ 无 `待测` 残留**)+ `OBS-13/14/15` + 探针新项 |
### 3.2 明确**不动**的(⛔ 防止顺手扩大)
1. ⛔ **不动任何生产值**:`RELAY_FAILOVER_*`(6 键)/ `HB_SEC` / burst 语义 —— 这是 **D1**,也是本线长期硬门。
2. ⛔ **不删 `/status`** —— 它是回滚链的一环(§7-②)。
3. ⛔ **不新增公网监听口**、⛔ 不改 `nft`、⛔ 不改 nginx(本单是纯应用层改动,**暴露面零变化** ⇒ §4.4 R5 未命中)。
4. ⛔ **不改 `src/net/relay/switcher.ts` 的冷却语义** —— presence 抖动抑制**独立成表**(§4.1-D7)。
5. ⛔ 不碰房间层 / 内容分发 / 打洞实现 / `src/worker/tunnel.ts`(死路径)。
6. ⛔ 不动 `04-调整方案/**` 档案正文(历史档案属性)。
7. ⛔ **不 commit / 不 push**(未获明确授权;序 ⑰ 那次授权是**当次一次性**的)。
8. 🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**(该值看似合法、实际自锁:归零会让"失败候选必须被排除"一起失效,实测 121–123 s 不切换)。
### 3.3 范围外登记(同 §0.4;执行棒**只登记、⛔ 不动手**)
1. **B 内容分发 / 块级内容寻址** —— 待拍板。
2. **房间层 presence 专属做法**(抽样降频 / idle / typing / bulk)—— 无载体。
3. 骨干节点服务范围(A 自用 / B 全网)—— 入口 §4 既有待拍板项,与 presence 无关。
---
## §4 决策点
### 4.1 已定项(**本棒自决,可推翻** —— 依据均为客观可判)
| # | 决策 | 值 | 依据 |
|---|---|---|---|
| **D1** | **落点层次** | **节点 / 端点 presence**(`src/net/relay/**`);⛔ 应用层 presence 不做 | §0.1 P1/P2 实测(零 `room|chat`、`presence` 零实现)+ 入口 §2 既有结论 + `清单 §五` 第 7 步把房间层整块后置 |
| **D2** | **权威归属** | **在线态权威 = relay 侧**(连接生命周期所在层);Manager 只**消费 + 缓存**,⛔ 不自己轮询推导(避免双权威) | ⚠️ **与 §3-4「权威状态单点」不冲突**:那条说的是**归属 / 租约 / 骨干资格**;presence 是**连接事实**,天然归属连接所在层。**租约仍只归 Manager 写** |
| **D3** | **一致性口径** | **最终一致**,允许 **5–15 s** 陈旧;**grace 10 s + 离线 debounce 30 s + TTL 安全网**(TTL 取 `HB_SEC` 现值的 3 倍,⛔ **不改 `HB_SEC`**) | `瓶颈落地方案 §1` 第 5/7 条;TTL 是"防网关崩溃漏事件"的安全网(第 1 条) |
| **D4** | **批合并 = 固定 1 s 窗口**(方案 A) | 每网关把本地变化攒起来,**每 1 s 一帧 pipeline 提交** | `瓶颈落地方案 §1` 第 2/4 条(业界量级对照:2 万条命令/秒 → 1 次 pipeline/秒)。**为什么不取"零延迟逐次推"**:presence 允许 5–15 s 陈旧 ⇒ 1 s 窗口完全够用,逐次推只会在抖动时制造帧爆炸(**正是要消灭的现象**) |
| **D5** | **`/status` 保留但降级** | 从"主路径"降为**兜底 / 对账**(保留代码与 nginx 放行,⛔ 不删) | 回滚链依赖它(§7-②);且它现在是 Manager 的唯一事实来源,一步切干净 = 单点失败 |
| **D6** | **订阅可见性** | 默认**只允许订阅同网络、且本机有权**的 host(沿用序③ 的逻辑名 `<net>/<hostId>` 与成员资格校验);**跨网络订阅 ⇒ 显式拒绝 + 拒绝计数 +1**,⛔ 不静默返空 | 权限只收窄(R5 方向);**静默返空 = 假绿**,本线已有判别器教训 |
| **D7** | **抖动抑制独立成表** | presence 的 grace / debounce **自成一张表**,⛔ **不复用** `switcher.ts` 的冷却表 | 两条语义不同("拨号失败"vs"在线态抖动"),混表会污染序⑦/⑧ 的冷却判据 |
### 4.2 真取舍(本单 = **空**)
三个曾考虑上抛的项(落点层次 / 批合并策略 / `/status` 去留)**均有客观判据可分出高下**(见 D1 / D4 / D5 的"依据"列)⇒ 按 §1 判据**自行拍掉、⛔ 不上抛**。
### 4.3 待拍板 = **空**
- presence 本身**已拍板**(用户「a 要做」)。
- **B(内容分发)不属本单**:它是**独立一项**、仍待拍板 ⇒ 本单只登记一行(§3.3-1),⛔ 不写成"待你拍板"塞进本节(避免把别人那题捆进来)。
### 4.4 权限影响评估(R5,**先评估后动手**)
| 维度 | 变化 | 结论 |
|---|---|---|
| 新增监听口 | **0**(帧走既有 wss 通道) | — |
| 新增凭据 | **0**(复用序③ 的节点密钥 / 成员资格) | — |
| 新增入站 | **0**(Manager / Worker **仍然只拨出**) | — |
| 可见面 | 订阅可见性**只收窄**(D6 同网络 + 有权 + 跨网络显式拒绝) | — |
> ⇒ **未命中 R5**,无需权限评估门禁,可直接开工。
### 4.5 与既有机制的对账(⛔ 不重复劳动、不重复造)
| 既有机制 | 与本单关系 |
|---|---|
| 序③ 身份 / 成员资格(`identityRequired`、`<net>/<hostId>`) | **复用**:订阅鉴权走它,⛔ 不新造一套 |
| R3 `RendezvousRegistry` / `RelayRendezvous`(relay 实时快照 + **15 s 陈旧回退**) | **被替换主路径**(保留为兜底):15 s 陈旧回退正是 D3 口径的现存实现,改造后由订阅推送取代 |
| R5 `DIAL / DIAL_ACK`(Manager 只拨出 + 本机回环池) | **不改**:presence 帧搭同一条通道,⛔ 不动拨号模型 |
| 序⑦ `directory.ts#listCandidatesFromDoc` / 序⑧ `switcher.ts`(冷却 + 一跳豁免) | **不改**:那是**地址选择**,与在线态是两件事(D7) |
| 序⑨ `waitUpOnStatus` + 终态早退(deadline 四段分解) | **不改**:那是**换址墙钟**;presence 缩短的是"Manager 多久知道谁在线",⛔ 别混为一谈 |
| 序⑤ 参数表 `OBS-01..12` + `scripts/overlay-probe.cjs` | **扩展**:新增 `OBS-13/14/15`(沿用"阈值⛔ 不许是脚本魔数"的纪律) |
| 序⑥ `RELAY_MAX_HOSTS` / `capacity.conf` | **不改**:帧数在 presence 口径下与容量无关 |
---
## §5 步骤 S0–S7(每步自带**一次可执行的验证**)
> ⚠️ **开工前**:先抢全局执行锁 `bash dsh-server-docs/scripts/handoff-guard.sh --claim-exec "<会话名>"`;抢不到 = 有会话在跑 ⇒ 停手(⛔ 不排队等)。
> ⚠️ **本单是执行棒,按单开工,⛔ 无须再出规划单**。
> ⚠️ **部署属 lane 内执行细节**(开发环境服务器,R8)⇒ 直接做,**动手前一句话说明在动什么**即可,⛔ 不上抛。
### S0 · 只读前置 + 基线采样(**零改动**)
逐条跑 §2 的 P1–P5 并**留档原文**;把 P4 的生产值快照与 P5 的三件套基线存到 `_中间产物_待清理/seq19/`。
**验证**:§2 五行输出全部与"期望输出"一致;若有偏差 ⇒ ⛔ 先停下报告,别改码。
### S1 · relay 侧 presence 权威表(**唯一真活**)
在 `src/net/relay/server.ts` 落 presence 表:**`open` ⇒ online;`close` ⇒ offline(先过 grace 10 s + debounce 30 s)**;**TTL 安全网**按 D3 兜底;同 `hostId` 多连接**按 device 聚合**(任一在线 = 在线)。
**判别器(必做,序⑤ 的教训)**:**不许只写日志** —— 必须有可断言计数 `subs` / `pushed` / `rejected`,并以 `status()` 暴露。
**验证(先红后绿)**:往 `test/relay.test.mjs` 追加用例 —— ①断开后 ≤ grace 仍 online、> grace+debounce 转 offline;②同 hostId 两连接断一条仍 online;③kill 掉通道后 TTL 内自愈。**先对旧 `lib` 跑一次拿红**,再改。
### S2 · 线协议帧(`wire.ts`)
新增 `SUB` / `UNSUB` / `PRESENCE` / `SNAP`:**帧号在既有分配表末尾追加**,⛔ 不改既有帧号语义(改了 = 与 47/106 上现役 `lib` 不兼容,热更窗口会断)。
**验证**:帧号唯一性用例(既有号集合 ∩ 新号 = ∅)+ 旧 lib 对新帧"**未知帧号 ⇒ 显式报错**"(⛔ 不静默丢弃)。
### S3 · 批合并 + 节流(1 s 窗口,D4)
每个网关把 1 s 内的变化**合并成一帧**(一帧带 host 数组);`SNAP` 供重连后**一次拿全量**(做法 ⑦:重连必须重拉)。
**验证**:单测断言 —— **稳态 0 帧**;**一次变化 ≤1 帧**;100 次连续变化(<1 s 内)合并后 **= 1 帧**。
### S4 · 消费侧接线(C1 / C2)
`RelayRendezvous` 改为**订阅为主路径**,`/status` 降为兜底;**C1** `src/web/server.ts`(`refreshOverlay` 一带)、**C2** `src/worker/relay-tunnel.ts` / `src/worker/agent.ts` 同族接线(**实测无需改就写明,⛔ 不为对称而改**)。
**验证**:单测 —— 订阅生效后**不再发起 `/status` 轮询**(判别器 = `/status` 命中计数稳态 = 0);断掉订阅 ⇒ **自动回退 `/status`** 仍能拿到在线态。
### S5 · 观测落地(参数表 + 探针)
参数表新增:`PRESENCE_BATCH_MS` / `PRESENCE_GRACE_MS` / `PRESENCE_OFFLINE_DEBOUNCE_MS` / `PRESENCE_TTL_MS` / `PRESENCE_SUB_MAX`(**每个键都要有实测值,⛔ 无 `待测` 残留**)+ `OBS-13`(presence 帧率:稳态 0)`OBS-14`(首帧即全量 `SNAP`,⛔ 无 N+1)`OBS-15`(状态陈旧 p95 ≤ 15 s);`scripts/overlay-probe.cjs` 增同名新项。
**验证**:`node scripts/overlay-probe.cjs --table "<工作区根>/参数表_覆盖网络_20260917.md"` ⇒ 新项**能判 PASS 也能判 FAIL**(⛔ 只做过 PASS 的路径 = 未验证,序⑫ 的夹具模式可复用:**假红 / 假绿两侧都要能抓**)。
### S6 · 本机多实例真机验收(**唯一动服务的一步**)
用**已拍板的本机多实例**(用户原话「本机内存大 可以模拟多台」)起 **N ≥ 4** 个节点,实测:
① 稳态 presence 帧数 = **0**;② 一次上下线 = **≤1 帧**;③ 与旧路径(按 T 秒拉 `/status`)**帧数降幅 ≥ 1 个数量级**;④ 首帧即全量;⑤ 跨网络订阅 ⇒ **显式拒绝**且计数 +1。
**验证**:五项逐条给"命令 + 实测数字 + 判定";跑完**必须归零**(`systemctl restart dshs` 把 Manager 归位回 47)并复跑探针。
### S7 · 收口(部署 + 三件套 + 回填 + 指纹)
部署 = `npm run build`(Node 22)→ `scp` 产物到 47 `/opt/dshs/lib/` 与 106 `/opt/dshs-cluster/lib/`(relay 真身另在 `/opt/dsh-relay/lib/`,**三处都要铺**)→ `systemctl restart dshs`(+ 需要时 `dshs-relay`)——**属 lane 内,直接做**;随后跑零回归三件套 + 回填 §8 + 给指纹 + 推进 `接续入口_覆盖网络线_20260916.md` §0/§2 + 写工作区日志。
**验证**:§6 的 E9/E10/E11 全绿;`overlay-probe` **12/12 + 新项 PASS**;池口 **64**;三服务 `active`。
---
## §6 验收判据 E1–E11(命令 + 期望输出,**可被第三方复现**)
| # | 判据 | 命令 / 观测点 | 期望 |
|---|---|---|---|
| **E1** | **稳态 0 帧**(变化驱动,无变化不推) | 单测 + 真机 N≥4 静置 60 s | presence 帧数 = **0**(⛔ 非 0 即未达标) |
| **E2** | **一次变化 ≤1 帧**(批合并生效) | 单测:1 s 内 100 次变化 | 帧数 = **1** |
| **E3** | **降幅 ≥ 1 个数量级**(对旧路径) | 真机:旧口径 `N` 节点 × `/T` 秒拉 → 新口径变化驱动 | 降幅 **≥ 10×**(写明 N / T / 实测算式) |
| **E4** | **首帧即全量**(⛔ 无 N+1) | 订阅后第一帧 | = `SNAP`(**1 帧**完成,⛔ 非逐 host 拉取) |
| **E5** | **生命周期正确性** | 断连 ⇒ ≤ 10 s 仍 online、> 40 s 转 offline;`kill -9` relay ⇒ TTL 内自愈 | 三条全绿(**三个阈值均来自 D3,⛔ 不许改口径去凑判据**) |
| **E6** | **多设备聚合** | 同 hostId 两连接断一条 | 仍 **online** |
| **E7** | **最终一致**(⛔ 不上强一致) | 代码面检查 | **无**跨节点同步 RPC / 无分布式锁 |
| **E8** | **权限只收窄 + 显式拒绝**(判别器) | 跨网络订阅请求 | **显式拒绝** + `rejected` 计数 **+1**(⛔ 不静默返空) |
| **E9** | **零回归三件套** | `npm test`(Node 22)/ `--scene all`(**必须带 `--table`**)/ `overlay-probe`(**必须带 `--table`**) | ≥ **162/161/0/1** + **12 PASS / 0 SKIP / 0 FAIL** + **12/12 + 新项 PASS** |
| **E10** | **参数表口径可执行** | `grep -c "待测" 参数表_覆盖网络_20260917.md` + 新增键逐项 | `PRESENCE_*` **全部有实测值**;**指纹变化必须写明新值**(口径 = `sed '/^## §10 指纹/,$d' … \| md5sum`) |
| **E11** | **D1 自证** | 与 §2-P4 的快照逐项比对 | `RELAY_FAILOVER_*` / `HB_SEC` / burst **一字未变**;`grep -c 'COOLDOWN_MS=0' scripts/` = **0** |
---
## §7 回滚(三层,**均秒级**;⛔ 不含任何 `COOLDOWN_MS=0`)
① **代码级**(主回滚):`git` 恢复本单改动的文件(或从 `.bak-seq19-<YYYYMMDD-HHMMSS>` 副本还原)→ `npm run build`(Node 22)→ `scp` 铺回 47 / 106 / relay 三处 `lib` → `systemctl restart dshs`(+ `dshs-relay`)。**判据 = 探针回 12/12 + `--scene all` 12 PASS**。
② **消费侧回退**(不重启服务即可):删掉 S4 的订阅装配点 ⇒ `RelayRendezvous` **自动回退**到既有的「relay 实时快照 + 15 s 陈旧回退」路径(**D5 保留 `/status` 就是为了这条**)。**判据 = `/status` 命中计数恢复非零 + 在线态仍正确**。
③ **观测侧**:参数表新增键**保留但标注"未启用"**(⛔ 不删行 —— 删了会让 `OBS-13/14/15` 悬空)。
> ⚠️ 备份命名一律 `.bak-seq19-<YYYYMMDD-HHMMSS>`,落**原位同目录**;relay 侧另存 `/etc/dshs/relay-keys.json.bak-seq19-*`(若本轮未动密钥则**不备份**)。
> ⛔ **回滚路径里出现 `RELAY_FAILOVER_COOLDOWN_MS=0` = 违规**(该值自锁,实测 121–123 s 不切换)。
---
## §8 回报格式(执行会话**必须**回填的证据,逐条给"命令 + 原文输出 + 判定")
```
### 8.0 回报(序 ⑲ 执行棒 · <开始时刻>–<收口时刻>)
### 8.1 只读前置复现
P1–P5 命令 + 原文输出(P4/P5 带取数时间);与 §2「期望输出」逐条对账(一致 / 偏差及处置)
### 8.2 S1–S7 逐步回报
每步:改了什么(文件 + 行级要点)/验证命令 / 原文输出 / 判定(✅ / ⚠️ 临界 / ❌)
⛔ 不许写"已完成"三字了事;**每步必须有一次可执行验证的原文**
### 8.3 验收 E1–E11 对账表
| # | 判据 | 命令 | 实测 | 判定 |
(❌ 项必须写:卡在哪 / 已做到哪一步 / 什么条件一出现必须回头)
### 8.4 真机降幅实测(E3)
N = ? / 旧口径算式的取数 / 新口径实测帧数 / 降幅 = ?
### 8.5 D1 自证
`RELAY_FAILOVER_*`(6) / `HB_SEC` / burst 改前 vs 改后逐值对比;`grep -c 'COOLDOWN_MS=0' scripts/` 实测值
### 8.6 部署与收口
build RC / scp 三处路径与产物 md5 / 重启了哪些单元 / 归零后三件套复跑值 / 池口数
### 8.7 参数表变更
新增键与实测值;**指纹:旧 → 新**(口径 = `sed '/^## §10 指纹/,$d' … | md5sum`)
### 8.8 遗留与回头条件
逐条列出;**若某条命中 §9 的回头条件 ⇒ 必须写明"命中第 N 条"并停下报告**
### 8.9 本单指纹(可复现)
`sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum` = ?(回填后**应保持不变**:§8 之后的内容不影响前缀)
```
---
## §8★ 回报(序⑲ 执行棒 · **已回填** · 2026-09-17 17:28–18:16)
> 本节 = 按上面 §8 的格式回填的**实测证据**(原文 + 命令)。所有输出取自本棒实跑,⛔ 无一条为推断。
### 8.0 基本信息
| 项 | 值 |
|---|---|
| 会话名 | `覆盖网络线-序19执行棒-presence`(全局执行锁:抢到 → 收口时释放) |
| 起止 | 17:28 → 18:16(含 S6 真机 2 分钟 + `--scene all` 6 分钟) |
| 改动文件 | 6 个 `src` + 1 个测试 + 1 个探针 + 参数表(⛔ 未 commit · 未 push) |
| 边界自证 | ⛔ 未改任何生产值 · ⛔ 未新增公网监听口 · ⛔ 未改 nft/nginx · 🔴 全程未出现 `COOLDOWN_MS=0` |
### 8.1 只读前置复现(S0)
| # | 命令 | 实测原文 | 与 §2 期望 |
|---|---|---|---|
| P1 | `grep -rilE "\b(room\|chat)\b" src \| wc -l` | `0` | 一致(零应用层) |
| P2 | `grep -ril presence src` | `src/net/relay/placement.ts`(仅注释) | 一致(只读缓存待替换已成事实) |
| P3 | `grep -n "req.url === '/status'" src/net/relay/server.ts` | `413`(`head -2` 证输出非空) | 一致 |
| P4 | `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` | `8f08e74b026e6e5b5e1b3db813f031ae` | 一致 ⇒ **D1 自证成立**(生产值一字未改) |
| P5 | `npm.cmd test` / `overlay-probe --table` / `--scene all --table` | `161 pass / 0 fail / 1 skip` / `12/12 PASS` / `12 PASS / 0 FAIL` | 一致(基线) |
**P4 生产值快照(逐值留档)**:`HB_SEC=15`(`server.ts:60`) · `HALF_OPEN_MS=37500` · `RELAY_FAILOVER_MIN_ATTEMPTS=3` · `GRACE_MS=15000` · `COOLDOWN_MS=300000` · `DEADLINE_MS=30000` · `CHECK_MS=2000` · `UP_TIMEOUT_MS=12000` · `GRACEFUL_BURST_MS=15000` · `EXEMPT=1`;`grep -c 'COOLDOWN_MS=0' scripts/` = **0**。
### 8.2 S1–S7 逐步回报
| 步 | 改了什么(文件 + 行级要点) | 验证命令 | 实测原文 | 判定 |
|---|---|---|---|---|
| **S1** | `server.ts`:`PresenceState`(`conns: Map<connId,lastActMs>` 逐连接判死)+ `presenceTouch/presenceDrop/presenceDevices/presenceEntry/publishPresence/flushPresence/presenceSnapshot` + 计数 `subs/pushed/snaps/rejected/statusHits` | `node --test --test-name-pattern="T(25\|29\|31) " test/relay.test.mjs` | 先红 `0 pass / 7 fail` → 后绿 **7/7** | ✅ |
| **S2** | `wire.ts`:**末尾追加** `SUB 0x10 / UNSUB 0x11 / PRESENCE 0x12 / SNAP 0x13`(既有 0x01–0x0f 语义一字未动) | T26(帧号唯一 + 未知帧号 0x99 必须显式拒绝) | `close 1008` + `authFailed 0→1` | ✅ |
| **S3** | `server.ts`:`presenceBatchMs` 批合并出口(1 s 窗口、一帧带数组)、`SNAP` 首帧全量、TTL 安全网逐连接摘除 | T27(6 台同窗口 ⇒ 1 帧 6 条)/T29(幽灵连接 TTL 自愈) | `Δ帧=1 条目=6` / 离线自动收口 | ✅ |
| **S4** | `client.ts`(订阅侧:`subscribePresence/presenceStatus/presenceOf/…`,失败 ≥2 次 ⇒ `unsupported` 停止再试)+ `rendezvous.ts`(`presence` 为**主路径**,`undefined` 才回落 `online`)+ `web/server.ts`(装配订阅 + `refreshRelay` 挂起 + `addressOf` ② 订阅落点) | T30(订阅新鲜 ⛔ 不读兜底;`undefined` 必须回退;两台 relay 之间**无**同步) | `calls.length = 0 / 0 / 1`;`s2.presence.length = 0` | ✅ |
| **S5** | 参数表 §3.6 新增 `PRESENCE_*` 五键 + §6 新增 `OBS-13/14/15` + 四阈值键;`overlay-probe.cjs` 新增三条**真判据**(含 `--status-fixture-2` 双快照) | 夹具五连跑(good + bad13/14/15/16) | good:三条全 `PASS`;bad13 `Δpushed=5 ⇒ FAIL`;bad14 `snaps=0 ⇒ FAIL`;bad15 `p95=60000 ⇒ FAIL`;bad16 口径漂移 ⇒ FAIL | ✅ |
| **S6** | 无改动(只跑现场) | `_tmp_seq19_s6.mjs`(**生产默认时序零注入**,N=6 本机多实例) | **8/8 ✅**(明细见 §8.4) | ✅ |
| **S7** | `npm run build` → `scp` 四处 `lib/` → 重启 4 个单元 | 见 §8.6 | build `RC=0`;四处 md5 同值;`dshs/dshs-relay/dshs-worker/dshs-pg` 全 `active` | ✅(OBS-09 除外,见 §8.8-1) |
**S1 的"先红后绿"原文**:`git stash push -m seq19-red -- <6 个 src 文件>` → 重建 `lib` → 跑 7 个新用例 = `0 pass / 7 fail`(`RED_RC=1`);`git stash pop` + 定点 LF 还原 → 重建 → **7/7 pass**。
### 8.3 验收 E1–E11 对账表
| # | 判据 | 命令 / 观测点 | 实测 | 判定 |
|---|---|---|---|---|
| **E1** | 稳态 0 帧 | 单测 T25/真机 S6 静置 60 s/探针 `OBS-13` | `Δ帧=0`(期间 24 个心跳帧在流、`lastSeenAgoMs≈101ms`)/真机 `Δpushed=0` | ✅ |
| **E2** | 一次变化 ≤1 帧 | T25/T27/S6 ② | `Δ帧=1`(上线、下线、6 台同窗口各 1 帧) | ✅ |
| **E3** | 降幅 ≥10× | S6 ③(算式见 §8.4) | 字面口径 **63×**;同消费者口径 10.5×;稳态 ∞ | ✅ |
| **E4** | 首帧即全量 | T25/S6 ④/真机 `/status` | `snapFrames=1`;`snaps 0→1`;真机 `snaps=2 = 订阅次数` | ✅ |
| **E5** | 生命周期正确性 | T25/T29/S6 ②a | 断连→改口 **40985 ms**(grace 10 s+debounce 30 s);TTL 幽灵自愈 | ✅ |
| **E6** | 多设备聚合 | T25(连接更替) | 顶旧接新期间 `online=true` 且 `devices=1`、**零新增帧** | ✅ |
| **E7** | 最终一致(无跨节点同步) | T30 尾段(两台 relay 独立) | `s1.presence=1 / s2.presence=0` | ✅ |
| **E8** | 权限只收窄 + 显式拒绝 | T28/S6 ⑤ | 客户端 `rejected=1`、服务端 `rejected 0→1`、状态回 `idle` | ✅ |
| **E9** | 零回归三件套 | `npm test` / `--scene all` / `overlay-probe` | **169/0/1**(≥162/161 ✅);`--scene all` = 见 §8.8-4;探针 **15 PASS / 1 FAIL** | ⚠️ **1 红**(`OBS-09`,见 §8.8-1) |
| **E10** | 参数表口径可执行 | `grep -c 待测` + 新键逐项 | `PRESENCE_*` **全为实测值**(⛔ 无 `待测`);指纹 `8f08…→ 13de5f9b77c486d71e5b83ec909b17b2` | ✅ |
| **E11** | D1 自证 | 与 §2-P4 快照逐项比对 + `grep -c 'COOLDOWN_MS=0' scripts/` | 六项生产值**一字未变**;禁用串计数 **0** | ✅ |
### 8.4 真机降幅实测(E3)
**现场**:本机多实例,`N = 6` 台 worker + 1 台订阅方,relay 用**生产默认时序**(零注入),窗口 `W = 104 s`(含一次下线 + 一次上线)。
**旧口径取数**:`src/web/server.ts:76 RELAY_STATUS_POLL_MS = 5000` ⇒ 同一消费者每 5 s 拉 1 次。
- 字面口径(交接单 §6 E3 原文「`N` 节点 × `/T` 秒拉」):`6 × (104 / 5) = 126` 次 → 新口径实测 **2 帧** ⇒ **63.0×**
- 同消费者口径(同一台 Manager 轮询):`104 / 5 = 21` 次 → 2 帧 ⇒ **10.5×**
- 纯稳态(60 s、零变化):旧 `12` 次 vs 新 **0 帧** ⇒ **∞**
**判定**:✅(≥10×)。⚠️ 临界说明:新口径的**下限**是"变化次数 × 1 帧"(与 N **无关**),而旧口径与 N 成正比 ⇒ N 越大降幅越大、变化越稀疏降幅越大;本窗口把"变化"人为压到 104 s 内两次(极端档)仍达 10.5×。
### 8.5 D1 自证
| 值 | 改前 | 改后 |
|---|---|---|
| `HB_SEC` | 15 | 15 |
| `HALF_OPEN_MS` | 37500 | 37500 |
| `GRACE_MS` | 15000 | 15000 |
| `COOLDOWN_MS` | 300000 | 300000 |
| `DEADLINE_MS` | 30000 | 30000 |
| `GRACEFUL_BURST_MS` | 15000 | 15000 |
| `RELAY_FAILOVER_EXEMPT` | 1 | 1 |
| `grep -c 'COOLDOWN_MS=0' scripts/` | 0 | **0** |
### 8.6 部署与收口
- `npm run build`(Node 22)= **RC 0**;`npx tsc --noEmit` = **RC 0**。
- 铺码 **四处**(产物 md5,改后值):`server.js = 182a7645f8ad3e8f240737b9dab91028`,`index/wire/client/rendezvous.js = 2a688424… / 467d38bd… / 46a025ff… / e851667d…`,`web/server.js = a360c7f4…` —— 47 `/opt/dshs/lib/net/relay/` + 47 `/opt/dsh-relay/lib/net/relay/` + 106 `/opt/dshs-cluster/lib/net/relay/` + 106 `/opt/dsh-relay/lib/net/relay/`(另 47 `/opt/dshs/lib/web/server.js`)⇒ **远端 md5 与本机逐条相同**。
- 重启单元:47 `dshs-relay`→`dshs-worker`→`dshs`;106 `dshs-relay`→`dshs-worker` ⇒ `dshs / dshs-relay / dshs-worker / dshs-pg` **全 active**。
- 归零复跑三件套:`npm test` **169/0/1**;`overlay-probe --table` = **15 PASS / 1 FAIL**(唯一红项 `OBS-09`);池口 **64**(`DIALER_POOL`,`ss` 实测 25000–25063 全绑);`--scene all` = 见 §8.8-4。
- 真机新路径证据(47 relay `/status`):`presenceTiming = {graceMs 10000, offlineDebounceMs 30000, batchMs 1000, ttlMs 45000, subMax 0}`;`counters = {subs 1, pushed 3, snaps 2, rejected 0}`;Manager 日志 `[relay-client] presence SNAP 2 条(scope=all)`。
### 8.7 参数表变更
新增键(**全部实测/可复算,⛔ 无 `待测`**):`PRESENCE_BATCH_MS=1000` · `PRESENCE_GRACE_MS=10000` · `PRESENCE_OFFLINE_DEBOUNCE_MS=30000` · `PRESENCE_TTL_MS=45000` · `PRESENCE_SUB_MAX=0` · `PRESENCE_STEADY_FRAMES_MAX=0` · `PRESENCE_SAMPLE_HITS_MIN=1` · `PRESENCE_SAMPLE_GAP_MS=3000` · `PRESENCE_STALE_P95_MAX_MS=15000`;新增观测行 `OBS-13/14/15`。
**指纹(口径 = `sed '/^## §10 指纹/,$d' … | md5sum`):`8f08e74b026e6e5b5e1b3db813f031ae` → `13de5f9b77c486d71e5b83ec909b17b2`**。
### 8.8 遗留与回头条件
1. 🔴 **`OBS-09` 红 ⇒ 命中 §9-8**(**已停下报告**)。**卡在哪**:`OBS-09` 要的是**用户实例面**(47 `127.0.0.1:20000`、对端 21000 经 relay),而 S7 规定的 `systemctl restart dshs`(+ 106 `dshs-worker`)**会把 `dsh-114801-*.scope` / `dsh-100002-*.scope` 一起收掉**;平台是**按需拉起**(拉起的唯一触发 = 用户真去打开自己的工作区),⛔ **无 reconciler**(重启后 4 分钟仍 `inactive`,Manager 日志零 spawn 记录)。⇒ **S7 的重启步骤与 E9 的探针判据在"无用户访问"时互相冲突**。**已做到哪一步**:探针 15 PASS / 1 FAIL,其余 11 项基线项全 PASS(`OBS-11` 集合判据含 nft 与监听面全清)。**什么条件一出现必须回头**:任一用户访问自己的实例页(实例被拉起)后重跑 `overlay-probe --table …` ⇒ `OBS-09` 应回 PASS;若拉起后**仍红** ⇒ 属真回归,立即回头查 `addressOf` 链路。
2. ⚠️ **本棒实测踩到并已修的假死(重要,已补测)**:`presenceTouch` 原在 `ensureEndpoint` **之前**、且 `localPort` 是 `listen(0)` 回调里**异步**才落地的 ⇒ 首帧推出去的落点是 `0`;而发布只在"在线态翻转"时发生 ⇒ 那个 `0` **永远修不回来** ⇒ 订阅方 `addressOf` 查不到落点 ⇒ **页面打不开且不报错**。修法:① `presenceTouch` 移到 `ensureEndpoint` 之后;② 落点落地时(`listen` 回调)与端口增删时**强制重推**(`publishPresence(..., force)`)。**新用例 `T32` 先红后绿覆盖**(先红实测 `localPort=0`)。真机复验:`ops/w-106` 的 `localPorts=[{port:19000, localPort:35289}]`(**非 0**)。
3. ⚠️ **已登记的既存缺陷(本单范围外,⛔ 未动)**:`web/server.ts` 的 `translateEndpoint` 走了与 `addressOf` **不同的键口径**(表键是逻辑名、传入裸 `hostId` ⇒ 恒 `undefined`)⇒ 该分支的订阅回退**故意未加**(加了也是死代码);要改须另立单(R7)。
4. ✅ **`--scene all` 本轮结果 = 12 PASS / 0 SKIP / 0 FAIL**(唯一"附带影响":幕 4 前置含一次 Manager 重启 ⇒ 可能再次收掉实例 scope;**未出现 FAIL**)。
5. ⏳ **`translateEndpoint` 键口径既存缺陷**(见上第 3 条)⇒ 建议并入序 ⑳ 或另立单。
6. ⚠️ **动态值提醒**:`w-106` 的 relay 落点 `localPort` 是**动态值**(本轮 = 35289),⛔ 不得写死进参数表。
### 8.9 本单指纹(可复现)
`sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum` = **`f612858344077420cff5f1f9ef1c942f`**(回填后**保持不变** ✅ —— §8 之后的内容不影响前缀)。
### 8.10 序 ⑳ 执行棒回报(2026-09-17 18:26–18:5x · OBS-09 口径修正 + presence 真机收益量化)
> 本棒四项:① OBS-09 口径 ② presence 真机收益量化 ③ 收口复验 ④ `translateEndpoint` 键口径缺陷处置。
> ⚠️ 本棒**只碰两个文件**:代码仓 `scripts/overlay-probe.cjs` + 工作区根 `参数表_覆盖网络_20260917.md`。**`src/**` 零改动**。
#### ① `OBS-09` 口径修正(产物 = `scripts/overlay-probe.cjs`)
**新口径 = 在册 ⇒ 必须可达**:在册判据 = `/status.endpoints[]` 中存在该实例端口**且** `online === true`;**在册** ⇒ 实例面码 ∈ `PROBE_CODE_SET`(否则 FAIL);**不在册** ⇒ **SKIP**(⛔ 不是 PASS —— 那会掩盖"该在册却掉出端点表",那一类由 `OBS-01`/`OBS-08` 守)。
新增夹具参数 `--instance-fixture <本机码>,<对端码>`(实例面码**不在**任何既有夹具里 ⇒ 必须新增;⛔ 无此参数时在册仍判 ⇒ 会 FAIL 并点名,属**契约面**、故意如此)。`add()` 增加显式 `skip` 位;`judgeObs09()` 提到分支外 ⇒ **两种模式都判**(否则夹具证不了"能判 PASS 也能判 FAIL")。
**两侧夹具实证(三段原文;夹具 = 47 上抓的**实况** `/status` + `ss` + `nft -j`)**
| 侧 | 输入 | 原文输出 | 退出码 |
|---|---|---|---|
| **A 不误红** | 端点表**补上** 20000/21000 两条在线条目 + `--instance-fixture 401,401` | `⚠️ FIXTURE PASS OBS-09 在册实例面 本机:20000=401 w-106:41695=401 (阈值 ∈ {200,401})|不在册 0 项` | **0** |
| **B 能判真红** | 同上夹具 + `--instance-fixture 502,000` | `⚠️ FIXTURE FAIL OBS-09 … |❌ 本机:20000=502 w-106:41695=0`;`❌ 1 项红:OBS-09` | **1** |
| **C 不在册 ⇒ SKIP** | 实况端点表(**只有** `w-106:19000`) | `⚠️ FIXTURE SKIP OBS-09 在册实例面 无 (阈值 ∈ {200,401})|不在册 2 项(记 SKIP 的依据):本机:20000,对端:21000` | **0** |
| **D 契约面** | 在册但**不给** `--instance-fixture` | `⚠️ FIXTURE FAIL OBS-09 … 本机:20000=NaN …` | **1** |
**真机(非夹具)**:`SKIP OBS-09 在册实例面 无 (阈值 ∈ {200,401})|不在册 2 项:本机:20000,对端:21000`、**rc=0** —— 修前该项**恒红**(序⑲ 的唯一红项),现已转为"明确的环境态"。
**自检**:`grep -cE "[0-9]{3,}" scripts/overlay-probe.cjs` = **0**(零数字纪律未破)|`node --check` OK。
**参数表**:§6 `OBS-09` 判据格已改写 ⇒ 指纹 `13de5f9b77c486d71e5b83ec909b17b2` → **`659c08949c193102f774822905fde3b7`**(§6 **只改判据文字、⛔ 无数值改动**)。
#### ② presence 真机收益量化 —— 🔴 **实测收益 ≈ 0(设计目标未达成)**
**算式**:`R_base = 60 000 / RELAY_STATUS_POLL_MS(5000) = 12 次/分钟`;`R_now = (ΔstatusHits − 探针自身读数) / 窗口分钟`;`降幅 = R_base / R_now`。
| 口径 | 实测 | 降幅 |
|---|---|---|
| 60 s 窗口(47 relay,T0=1789641041) | `statusHits 191→204`(Δ=13,自身 2)⇒ **11 次/分钟** | **1.09×** |
| 12.4 min 窗口(T0=1789641041 → 1789641786) | `statusHits 191→334`(Δ=143,自身 8)⇒ **≈10.9 次/分钟** | **1.10×** |
| 门开关占空比(journal 时间线) | 35 min 内 **11 次切换**;挂起窗口 7/20/20/10/45 s = 102 s / 2040 s ⇒ **5.0%** | 理论 **1.05×**(与实测吻合) |
**🔴 根因(代码级)**:`presenceFresh()`(`src/net/relay/client.ts:593-600`)= `state==='subscribed' && now − max(lastSnapAt,lastPushAt) ≤ PRESENCE_TTL_MS(45 s)`;而**稳态下 relay 一帧都不推**(`pushed=1 / snaps=1` 自 relay 重启起未增,实测 `Δpushed=0`)⇒ 45 s 后**必然过期** ⇒ `pollSuspended` 门重开、轮询恢复。**"零帧"(收益的来源)与"帧新鲜度"(门的判据)互相打架** ⇒ 已登记为在册缺陷 **P-1**(`交接单_观测口径与在册缺陷_20260917.md` **§13**)。
**⚠️ 口径勘误(必读)**:序⑲ 的"降幅 63× / 10.5× / ∞"是**帧**的口径(`pushed`),**不是 `/status` 轮询降幅**;本棒给出后者 = **1.10×**。
**⚠️ 附带口径提醒**:`OBS-13` 的"活性证明 `ΔstatusHits ≥ 1`"由**探针自己两次读**即满足 ⇒ 它**不能**证明"轮询停了"(真机实测 `Δ=3/3 s`,其中 2 次是探针自身)。⛔ 别把它读成收益证据。
#### ③ 收口复验三件套 + 归零
| 项 | 结果 | 基线 |
|---|---|---|
| `npm test`(Node 22) | **170 tests / 169 pass / 0 fail / 1 skip** | = 序⑲ 基线 ⇒ **零回归** |
| `overlay-probe --table`(真机) | **14 PASS / 0 FAIL / 1 SKIP**,rc=0 | 原为 15 PASS / **1 FAIL** ⇒ OBS-09 转 SKIP |
| `--scene all --table` | **12 PASS / 0 SKIP / 0 FAIL**,rc=0(幕1-A 18898ms/幕4-A 23183ms+豁免回切 20887ms) | = 序⑲ 基线 ⇒ **零回归** |
| 收口归零 | 47 `dshs`=active、`dshs-relay`=active、**池口 64**;Manager 已归零回 47 | ✅ |
⚠️ **106 单元状态未取到**(`ssh 106.54.21.172` rc=255 = MaxStartups 限流,本线已知)⇒ ⛔ 放宽 sshd 命中 **R5** ⇒ **只报告**。
#### ④ `web/server.ts#translateEndpoint` 键口径缺陷 —— **判定 = 另立单(本棒未动手)**
**证据(代码级)**:`hostVia.set(name, row.via)`(`:733`,`name = logicalName(networkId, row.id)` = `ops/w-106`,**键 = 逻辑名**)vs 调用方 `src/supervisor/remote-spawner.ts:321 this.translateEndpoint(host.hostId, raw)`(**裸 hostId**)⇒ `:831 hostVia.get(hostId)` **恒 `undefined`** ⇒ **早退原样透传** ⇒ 该闭包(含 `:840` 的 `${hostId}:${ep.port}` 快照回退,键同样应为逻辑名;对照 `:689`/`:700` 用的就是逻辑名)**整体是死分支**。
**影响面**:仅 `via='relay'` 的**实例面**(今天 = `w-106`);现场 `ss -lntp | grep -c ':21000'` = **0**、`journalctl -u dshs --since -6h | grep -c ':21000'` = **0** ⇒ **当前零实际影响**。
**为何本棒不修**:① 修它 = 让一条**当前恒被跳过**的翻译路径**重新生效**(数据面行为变化);② 该闭包是 `buildServer` 内部闭包、**无法先红后绿**,须先抽纯函数(= 一次小重构);③ 端到端验证需起 `w-106` 实例(当前无 reconciler、实例不自回)⇒ **本棒无验证条件**。⇒ 登记为 **P-2**(缺陷单 §13,含候选修法与判据),交给下一棒。
#### ⑤ 边界自证 + 指纹
**边界**:⛔ 未改任何生产值(`RELAY_FAILOVER_*` / `HB_SEC` / burst 逐项未变;参数表改动**仅在 §6 判据文字**)|⛔ 未新增公网监听口 / 未改 nft·nginx|🔴 `COOLDOWN_MS=0` 计数 **0**|⛔ 未 commit / 未 push(`scripts/overlay-probe.cjs` 仍为 `M`)|文件面 = 代码仓 `scripts/overlay-probe.cjs`(`md5 4cbe34f1ffc51cfab78f0540224aa388`)+ 工作区根 `参数表_覆盖网络_20260917.md`;`src/**` 零改动。
**指纹**:参数表(口径 = `sed '/^## §10 指纹/,$d' … | md5sum`)**`659c08949c193102f774822905fde3b7`**(旧 `13de5f9b77c486d71e5b83ec909b17b2`)|本单 §8 前缀 **`f612858344077420cff5f1f9ef1c942f`(未变 ✅)**|探针 `4cbe34f1ffc51cfab78f0540224aa388`。
---
### 8.11 序㉑ 执行棒回报(2026-09-17 19:0x–19:4x · **P-1 与 P-2 均已修**)
> 口径依据 = 入口 §2「本轮动作」+ 缺陷单 §13。本棒四项:① 修 P-1(门判据)② 修 P-2(键口径 ⇒ 抽纯函数)③ 零回归三件套 ④ 收口。
#### ① P-1 已修 —— **门的判据从「载荷年龄」改为「订阅已建立 ∧ 链路活着」**
**我选了什么(可推翻)**:候选 **A**(任何入站帧含心跳即算"链路活着")。
- ⛔ **不选 B(relay 定期重推 `SNAP`)**:那会把**稳态零帧**这条设计属性打掉 —— 序⑲ **E1** 的机器判据就是 `Δpushed ≤ 0`,`OBS-14` 还依赖 `snaps ≤ pushed` 这个不变量;B 让 `snaps/pushed` 在稳态持续增长 ⇒ **直接推翻本单自己的验收口径**(R11:净变差)。
- ⛔ **不选 C(拉长 TTL)**:只是把竞速推远(45 s → 更长),门迟早仍会自开 ⇒ 治不了"把**没有变化**读成**没有数据**"这个语义错位。
- ✅ **A 的语义理由**:镜像的**有效性**由 ① 订阅已建立(拿到过首帧 `SNAP`)② 通道**有序可靠**(WS/TCP)共同保证 —— relay 侧每一次变化**必然**按序到达,**载荷的年龄与内容正确性无关**;只有**链路死**才让镜像失效,而链路死由半开巡检(`2.5 × hbSec` 无任何入站帧 ⇒ 主动断链)兜住,断链即 `state ≠ 'subscribed'` ⇒ 门自动打开、回退 `/status`(兜底路径**一行未删**)。
- ⚠️ **已知残余 + 回头条件**:relay 侧**静默**清掉订阅而 socket 仍活 ⇒ 本判据察觉不到。当前代码里**不可达**(唯一清空 `session.subs` 的是显式 `UNSUB`;relay 重启 / 会话回收都会断 socket)。已按 §9 第 **10** 条登记。
**改法(代码级)**:`src/net/relay/client.ts`
| 位置 | 改动 |
|---|---|
| `presenceFresh(ttlMs?)` | 判据 = `state === 'subscribed' && lastSnapAt !== 0 && (now − lastFrameAt) ≤ presenceLinkSilentMaxMs(ttlMs)`(`lastFrameAt` = **任何**入站帧,含心跳) |
| `presenceLinkSilentMaxMs()`(新增私有) | 上界 = `max(服务端 TTL, 半开阈值)` —— 🔴 **不得比链路巡检更紧**,否则门会比"链路真的死了"更早打开(= 把 P-1 换个方向重犯);测试档 TTL 被压到 3 s、心跳仍 15 s ⇒ 只用 TTL 会造**假红** |
| `halfOpenMs()`(新增私有) | 半开阈值唯一来源(`startHealthWatch` 与本判据共用,⛔ 不引入第二个时间口径) |
| `RelayPresenceStatus`(+3 字段) | `lastInboundAgoMs` / `linkSilentMaxMs` / `fresh` —— 让"门为什么开着/关着"**可判**(判别器纪律,⛔ 不静默) |
#### ② 先红后绿(原文级)
把 `presenceFresh()` **临时改回旧判据**(`max(lastSnapAt,lastPushAt)`)重建后跑新用例:
```
not ok 1 - T33 P-1:稳态零帧下门不得自己重开(判据 = 订阅已建立 ∧ 链路活着,⛔ 不是载荷年龄)
error: |-
载荷 5243ms 没来、但链路活着(入站静默 217ms ≤ 上界 3000ms)⇒ 门必须保持关闭
# pass 0 / # fail 1
```
改回新判据重建后:`ok 1 T33` / `ok 2 T34`(**2 pass / 0 fail**)。用例清单(`test/relay.test.mjs`):
`T33` 稳态超过 TTL 门**不得**自开(旧实现必红的那条);`T34` **反向失败关闭**(未订阅 / 已退订 / 被拒 / 断链 ⇒ 一律不新鲜 —— 少这条,"永远返回 true"也能让 T33 绿)。
⚠️ `T33` 用夹具前提 `hbSec: 1`:测试档 TTL 3 s < 生产心跳 15 s,不压心跳时 relay 的 `presenceDevices`(`now − conns[ts] ≤ ttl`)会在 3 s 后把连接判死 ⇒ 自己制造"离线→在线"**假帧**(生产 `TTL 45 s > 心跳 15 s`,不存在该组合)。
#### ③ 真机验收(47 relay,**同一口径 Before / After**)
算式:`窗口内 ΔstatusHits = 探针自身读数(窗口内 1 次)+ Manager 侧命中` ⇒ **`Manager 侧 = Δ − 1`**。
| 口径 | 窗口 | `statusHits` | Δ | Manager 侧 | 门开关 |
|---|---|---|---|---|---|
| **Before**(旧代码在跑,先测) | 66 s | `228 → 242` | **14** | **13**(≈ 11.8 次/分钟,= 基准 12/分钟 ✅) | 窗口内 0 行(旧口径下门基本常开,翻一次后不再翻) |
| **After**(铺新 lib + `systemctl restart dshs` 后) | 67 s | `247 → 248` | **1** | **0** ✅ | 窗口内 **0 行** |
**门开关原文(`journalctl -u dshs | grep overlay-presence`,重启后至今 5+ 分钟、远超 TTL 45 s)**:
```
Sep 17 19:09:53 … [overlay-presence] 订阅新鲜 ⇒ `/status` 轮询**挂起**(兜底路径待命)
--- 总行数: 1
```
⇒ 即"挂起"一次、**零次翻转**(旧代码在 TTL 边界必然翻转 ⇒ 轮询恢复)。旁证:两窗口 `Δpushed = 0`、`subs = 1`(稳态零帧、订阅在册)。
**部署**:本机 build ⇒ `lib/net/relay/{client,index,endpoint-target}.js` + `lib/web/server.js` scp 到 **4 处** —— 47 `/opt/dshs/lib` / 47 `/opt/dsh-relay/lib` / 106 `/opt/dshs-cluster/lib` / 106 `/opt/dsh-relay/lib`,`md5` 全部 = 本机 `6c14d4d4…`;**只在 47 重启 `dshs`**(改动是 Manager 侧;relay 服务端路径未动,106 的文件待其下次重启自然生效)。
#### ④ 观测口径:新增 `OBS-16`(门的验收判据;⛔ 不能只看 `OBS-13`)
🔴 **为什么必须新增**:`OBS-13` 的 `ΔstatusHits ≥ 1` 由**探针自己两次读**即满足 ⇒ **证明不了"轮询停了"** —— 旧口径下门 95% 时间开着、轮询照旧在跑,而 `OBS-13` 仍然全绿(实测降幅 1.10× 却长时间没人发现,正是这条)。
**新判据**(`scripts/overlay-probe.cjs`,第三次 `/status` 采样,窗口 = `PRESENCE_GATE_WINDOW_MS`):
`subs ≥ 1` **且** `ΔstatusHits ≥ 1`(自身读数 = 活性证明)**且** `≤ PRESENCE_GATE_HITS_MAX`(= 1 ⇒ 除探针外**零**命中)。
**夹具两侧实证**(夹具 = 47 实况 `/status` + `ss` + `nft -j` 原文,仅改 `counters.statusHits`;新增 `--status-fixture-3`):
| 侧 | 输入 | 原文输出 | rc |
|---|---|---|---|
| **PASS** | 三连 `181 / 182 / 183`(窗口 Δ=1) | `⚠️ FIXTURE PASS OBS-16 门窗口 ΔstatusHits=1(≥ 1 = 探针自身读数|≤ 1 = 除探针外**零**命中),窗口 60000ms,期间 subs=1` | **0** |
| **FAIL** | 第三份 `194`(窗口 Δ=12 = 旧口径下的轮询量) | `⚠️ FIXTURE FAIL OBS-16 … ΔstatusHits=12 …`;`❌ 1 项红:OBS-16` | **1** |
⚠️ 夹具模式**必须**显式给 `--status-fixture-3`:缺省 = 与第二份同一份 ⇒ Δ=0 ⇒ 活性证明不成立 ⇒ FAIL 并点名(**契约面**,故意如此,与 `OBS-09` 的 `--instance-fixture` 同规则)。
**真机(非夹具)**:`PASS OBS-16 门窗口 ΔstatusHits=1 … 窗口 60000ms,期间 subs=1` ⇒ **15 PASS / 0 FAIL / 1 SKIP**、`rc=0`(基线 14/0/1 + 新项)。
⚠️ **副作用(已知、有意)**:真机探针一轮**多花 `PRESENCE_GATE_WINDOW_MS`**(窗口必须够长,否则"轮询在跑"与"轮询停了"在计数上分不开)。
#### ⑤ P-2 已修 —— `translateEndpoint` 键口径(**抽纯函数 ⇒ 才可能先红后绿**)
**病根**:控制面所有表(`hostVia` / `relayEndpoints` / 拨号池)键都是**逻辑名**,而调用方 `RemoteSpawner.translateEndpoint(host.hostId, …)` 只给得到**裸 hostId** ⇒ `hostVia.get(hostId)` 恒 `undefined` ⇒ 早退原样透传 ⇒ **整个闭包是死分支**(`:840` 的快照回退键同错;`:689`/`:700` 用的正是逻辑名,自证口径)。
**改法**:新增纯函数模块 `src/net/relay/endpoint-target.ts` = `relayEndpointTarget()`(四支判定:未知 host / 非 relay ⇒ **原样透传**;`via=relay` ⇒ ①拨号落点 ②快照落点 ③**`unreachable` 失败关闭**)+ `hostNameIndex()`(`dsh_hosts` 行 ⇒ `hostId → 逻辑名`);`src/web/server.ts` 的闭包改为先 `hostNameById.get(hostId)` 换逻辑名再调用它。
🔑 **两处设计要点**:① `dialedPort` 传 **thunk** —— `RelayDialer#localPortFor` **会按需绑池口 / 真的拨一次**,⛔ 非 relay / 未知 host 时不许碰它(`T35` 把这钉成断言);② 失败关闭分支**点名写日志**(`[overlay-endpoint] ⛔ … 判实例不可达`),因为原故障的表现只有"空响应 + 平台零日志"。
**单测**:`T35`(四支判定 + "⛔ 不误触发拨号池")、`T36`(`hostNameIndex` 口径 + **源码级守卫**:闭包⛔ 不得再出现 `hostVia.get(hostId)` / `localPortFor(hostId` / `relayEndpoints.get(\`${hostId}`,只看代码行不看注释)。
**端到端判据(本轮未做,按入口授权"起不起来就只做到单测")**:需 `w-106` 实例在跑(当前无 reconciler、不起就自回)。可执行判据 = ① 起实例后 `POST /api/dsh/enter` **200**;② `ss -lntp | grep -c ':21000'` 在 Manager 上仍为 **0**(翻译生效 ⇒ 拨的是 relay 回环落点,⛔ 不是 worker 侧口号);③ `journalctl -u dshs | grep -c overlay-endpoint` = **0**(失败关闭未被触发);④ 反例判据:若翻译未生效,Manager 会去连 `127.0.0.1:21000` ⇒ 连接被拒(本线 2026-09-16 踩过的"空响应 + 零日志"同族)。
✅ 顺带核实(DB,只读):`dsh_hosts` = `w-106|relay|ops`、`w-47|local|ops` ⇒ **w-106 就是走被修的那条分支**;且 `endpoint === undefined` 在 `proxy.ts:240` 得到的是**显式** `{ error: 'not_running', code: 404 }`(不是崩溃、也不是静默)⇒ 失败关闭语义干净。
#### ⑥ 零回归三件套(全绿,均带 `--table`)
| 项 | 结果 | 基线 |
|---|---|---|
| `npm test`(Node 22) | **174 tests / 173 pass / 0 fail / 1 skip**(= 170/169/0/1 + 新增 T33–T36) | 170/169/0/1 ⇒ **零回归** |
| `--scene all --table` | **12 PASS / 0 SKIP / 0 FAIL**,`rc=0`(幕1-A 19060ms/幕4-A 22543ms+豁免回切 16512ms/幕4-B/4-C 全绿) | 12/0/0 ⇒ **零回归** |
| `overlay-probe --table` | **15 PASS / 0 FAIL / 1 SKIP**,`rc=0`(`OBS-09` SKIP = 在册判据、`OBS-16` PASS) | 14/0/1 ⇒ **+1 项、零回归** |
| 收口归零 | 47 `dshs`/`dshs-relay` active、106 `dshs-relay`/`dshs-worker` active、Manager 归位 47、池口 76 行监听(`ss -lntn` 计数为 76 —— ⚠️ 与序⑳ 的"池口 64"口径**不同**:后者是 relay 自报的池容量 gauge,前者是本机监听行数,两者本就不同量,此处只作"未新增暴露面"的旁证,`OBS-11` 集合判据已绿) | ✅ |
#### ⑦ 边界自证 + 指纹
**边界**:⛔ 未改任何生产值(`RELAY_FAILOVER_*` / `HB_SEC` / burst 逐项未变;`PRESENCE_TTL_MS` **值未改**,只改判据语义)|⛔ 未新增公网监听口 / 未改 nft·nginx|🔴 `COOLDOWN_MS=0` 计数 **0**(含回滚/演练/夹具路径)|⛔ 未删 `/status` 兜底路径|⛔ 未改 `switcher.ts` 冷却语义|⛔ 未 commit / 未 push。
**文件面**:代码仓 = `src/net/relay/client.ts`(`c5d395395324e17edf9d269f34e8b3af`)|`src/net/relay/endpoint-target.ts`(**新增**,`24ac70edc52adec4928513459a3e0160`)|`src/net/relay/index.ts`|`src/web/server.ts`(`adf69e484cf00e95a132c7f6c0ae8c65`)|`test/relay.test.mjs`(`b925a672c5fb897f0598ef89d2f2bde8`)|`scripts/overlay-probe.cjs`(`61a9fc5623bb617c802daf604d3ce0f3`,旧 `4cbe34f1ffc51cfab78f0540224aa388`);工作区根 = `参数表_覆盖网络_20260917.md`。
**指纹**:参数表(口径 = `sed '/^## §10 指纹/,$d' … | md5sum`)**`42238175d84319ada99afa56d583f9db`**(旧 `659c08949c193102f774822905fde3b7`;本轮 = §3.6 +2 键、§6 +1 项 +2 阈值键、§3.6 增"P-1 口径变更"注)|本单 **§8 前缀 `f612858344077420cff5f1f9ef1c942f`(未变 ✅,§8.11 在截断点之后)**|本单全文件 md5(回填前)`b070e4228d061bb39a62d2aa0c11e416`。
**登记(⛔ 本棒未动手,只登记)**:**P-2b** —— `relayEndpointTarget` 目前只有"拨号落点 → relay 快照"两级;**P-1 修好后 `/status` 快照路径趋冷**(门长期挂起 ⇒ 快照很少刷新),当拨号池分不出槽位而**订阅推送里有落点**时仍会判失败关闭。候选 = 把 `presenceLocalPort`(订阅落点)插成第 ②'级(与 `addressOf` 的三级链对齐)。⚠️ 属**行为面新增**,按 R7「先报告后动手」登记,未在本棒实施。
---
### 8.12 序 ㉒ 执行棒回报(2026-09-17 19:33–20:0x · **P-2b 已修 + P-1 长窗复测通过**)
> 口径依据 = 入口 §2「本轮动作」+ 缺陷单 · 在册缺陷表 **P-2b** 行。本棒三件:① 修 P-2b ② P-1 修复后的**长窗(≥ 10 min)**真机复测 ③ 零回归三件套 + 收口。
#### ① P-2b 已修 —— 候选链由「两级」扩成「三级」,与 `addressOf` **逐级对齐**
**病根(一句话)**:`relayEndpointTarget` 只查 ① 拨号落点 → ② relay 快照;而 `addressOf`(`RelayRendezvous`)查 ① 拨号 → ② **订阅推送**(`presenceLocalPort`)→ ③ 快照。**P-1 修好之后**判据变成「订阅已建立 ∧ 链路活着」⇒ 订阅新鲜期**长期成立** ⇒ 快照(②)**趋冷**;此时若拨号池分不出槽位(跨网被拒 / 池满 / 尚未绑口),本判定会落到"两级都没有" ⇒ **判实例不可达(失败关闭)**,**而同一时刻 `addressOf` 明明能从订阅推送里答出落点** ⇒ **两条链漂移**:用户看到"实例打不开",日志里却什么都没有。
**改法(三个在册文件,⛔ 未越界)**:
| 文件 | 改动 |
|---|---|
| `src/net/relay/endpoint-target.ts` | 新增入参 `pushedLocalPort?: number`;`via` 联合类型加 `'pushed'`;判定顺序变 **① `dialed` → ② `pushed` → ③ `snapshot` → 失败关闭**;模块头部的判定表与"两级 → 三级"的理由写进注释 |
| `src/web/server.ts` | 闭包多传一行 `pushedLocalPort: name === undefined ? undefined : presenceLocalPort(name, ep.port)`(与 `addressOf` 的 ② 级**同一个函数、同一个顺序**);失败关闭的点名日志改成"拨号落点 / 订阅推送 / relay 快照**三级**都查不到" |
| `test/relay.test.mjs` | 新增 **T37**(三级逐支可判 + `0` 哨兵逐级下探 + 未知 host / 非 relay ⛔ 不碰拨号池)/**T38**(源码级守卫:闭包**必须**把 `presenceLocalPort(name, ep.port)` 传进判定 —— 无实例时该分支根本不执行,运行时断言抓不到) |
**先红后绿(原文级 · 同一份测试体)**:临时把 **build 产物** `lib/net/relay/endpoint-target.js` 里的 ②' 级整段去掉(源码不动)后跑:
```
not ok 1 - T37 P-2b:候选链三级(拨号 → 订阅推送 → relay 快照)逐支可判,⛔ 不误触发拨号池
error: |-
P-2b:拨号给不出时,订阅推送的落点必须先于 relay 快照被采用(与 `addressOf` 对齐)
+ actual - expected
{ kind: 'local',
+ port: 41000,
+ via: 'snapshot'
- port: 37057,
- via: 'pushed' }
# pass 0 / # fail 1
```
`tsc` 重建后:`T37` / `T38` **2 pass / 0 fail**。⇒ **红确实是"少这一级"造成的**,不是测试写歪。
#### ② P-1 长窗(706 s ≈ 11.8 min)复测 —— **门 0 次翻转、Manager 侧 0 命中**
算式(与序㉑ 同口径):`Manager 侧 = 窗口内 ΔstatusHits − 探针自身读数`;窗口开端读一次(计入 `T0` 读数本身)、末端读一次 ⇒ **自身读数 = 1**。**窗口内不跑探针**(探针的 `OBS-16` 自己会占一次窗口,会污染计数)。
| 口径 | 窗口 | `statusHits` | Δ | Manager 侧 | 门开关行数 | `Δpushed` / `Δsnaps` |
|---|---|---|---|---|---|---|
| **入册(短窗)** | 67 s | `247 → 248` | 1 | **0** | **0** | — / — |
| **本棒(长窗)** | **706 s** | `9 → 10` | **1** | **0** ✅ | **0** ✅ | **0 / 0** ✅ |
**相对基准的倍数**:基准 = `60 000 / RELAY_STATUS_POLL_MS(5 000)` = **12.000 次/分钟**;实测 raw Δ = `1 / (706/60)` = **0.085 次/分钟** ⇒ **141.2×**;**Manager 侧 = 0 次/分钟 ⇒ 无穷倍**(分母为零)。
**门开关计数原文**:`journalctl -u dshs --since @<T0> | grep -c overlay-presence` = **0**(窗口 706 s ≫ `PRESENCE_TTL_MS` 45 s ⇒ 若判据仍是"载荷年龄",这里必然 ≥ 1 ⇒ **长窗是对 P-1 修复的**独立**复核,不只是短窗的重复**)。
**旁证**:窗口两端 `subs = 1`(订阅在册)、`pushed = 1 → 1`、`snaps = 1 → 1` ⇒ 稳态零帧,正是 P-1 的病理前提。
#### ③ 零回归三件套(全绿,均带 `--table`;**部署后重跑同值**)
| 项 | 结果 | 基线 |
|---|---|---|
| `npm test`(Node 22) | **176 tests / 175 pass / 0 fail / 1 skip**(= 174/173/0/1 + T37/T38) | 174/173/0/1 ⇒ **零回归** |
| `--scene all --table` | **12 PASS / 0 SKIP / 0 FAIL**,`rc=0`(幕1-A 15561ms/幕4-A 23612ms + 豁免回切 21145ms) | 12/0/0 ⇒ **零回归** |
| `overlay-probe --table` | **15 PASS / 0 FAIL / 1 SKIP**,`rc=0`(`OBS-09` SKIP = 在册判据;`OBS-16` PASS `ΔstatusHits=1`) | 15/0/1 ⇒ **零回归** |
| 收口归零 | 47 `dshs`/`dshs-relay`/`dshs-pg` active |106 `dshs-relay`/`dshs-worker` active |监听行 **76** / 池口 **64** |`overlay-endpoint` 失败关闭点名 **0 行** | ✅ |
⚠️ **观察项(非回归,⛔ 未处置)**:`OBS-15` 在线态陈旧 `p95 = 14220 ms`(阈值 `≤ 15000 ms`,样本 2 条)—— **贴线但未越界**;同批另一次采样为 `5347 ms` ⇒ 样本少、抖动大。**本棒未改任何阈值 / 未放宽判据**,仅登记。
#### ④ 部署 + 回滚点
`npm run build` ⇒ `lib/net/relay/{endpoint-target,index}.js` + `lib/web/server.js` scp 到 **4 处**:47 `/opt/dshs/lib`+`/opt/dsh-relay/lib`、106 `/opt/dshs-cluster/lib`+`/opt/dsh-relay/lib` ⇒ **四根 md5 全同**:
```
36b2dd9953a615de9a3e148752dffe94 net/relay/endpoint-target.js
486419ba443b99dd734ca79e987ffdea net/relay/index.js
460d360e8c6196341935b2f9a2a1d408 web/server.js
```
**只重启 47 的 `dshs`**(改动是 Manager 侧)。**回滚点**(两机同路径)= 把本棒的两处 hunk **反向改回** → `tsc` → 得到的**精确旧产物**(⛔ 不是"凭记忆重写"):
```
/opt/dsh/backups/seq22-20260917-195602/{net/relay/endpoint-target.js,net/relay/index.js,web/server.js}
57e6c9ae81d64f6586d758f5dc021a4b endpoint-target.js(旧) 486419ba443b99dd734ca79e987ffdea index.js(未变)
74c077b33c6d8afdef980171c19eee26 server.js(旧)
```
#### ⑤ 本棒两条**环境/流程**教训(下一棒别踩)
1. 🔴 **106 的 ssh 必须用别名 `test106`**(专用密钥 `id_ed25519_test106`,`IdentitiesOnly yes`;参数表 `SSH_TARGET_106` 本就写着)。裸 `ssh root@106.54.21.172` ⇒ **`Permission denied (publickey)`** —— 本棒先误判成"MaxStartups 限流 rc=255"并据此放弃,后来抓 stderr 才看到真因,改用别名后**一次通过**。⛔ 后续任何一棒**不许再把 106 连通失败笼统归因成限流**。
2. ⚠️ **原生 `scp` 不认 `E:/…` 混写路径**(Git Bash 里 `cd E:/…` 能用,但 `scp 'E:/…/f'` 会 `No such file or directory`)⇒ 一律 **先 `cd` 到目录、再用相对路径**;本棒的"回滚点落盘"第一版就是因此静默空跑(只打印了一行空 md5),已重跑纠正。
#### ⑥ 登记(⛔ 本棒未动手)
- **E2E(P-2 + P-2b)仍未做** —— 47 上活跃实例 scope = **0**(`systemctl list-units 'dsh-*.scope'` 空;`:21000` 监听 = 0)⇒ 判据仍是本单 **§8.11-⑤** 那四条,**交序 ㉓**。
- 在册缺陷表**已清空**(P-1 / P-2 / P-2b 全部关闭);剩余只有 **§9 第 10 条**(回头条件,非在册缺陷)+ **E2E 缺口** + **两项待拍板**(`B` 内容分发 / 「Manager 重启后是否自动拉起既有实例」)。
### 8.13 序 ㉓ 执行棒回报(2026-09-17 20:08–20:4x · **P-2 / P-2b 的 E2E 已验收通过 + 在册缺陷表清空 + 线内可停**)
> 口径依据 = 入口 §2「本轮动作」+ 本单 **§8.11-⑤**(E2E 四条判据)。本棒两件:① **主 = P-2/P-2b 的 E2E** ② **次 = presence 线收口复核** ③ 零回归三件套 + 收口。
#### ① 前置(T0 = 2026-09-17 20:10:57,`epoch 1789647057`)
| 项 | 值 |
|---|---|
| 47 单元 | `dshs` / `dshs-relay` / `dshs-pg` = **active** |
| `dsh_hosts`(只读) | `w-106 → http://127.0.0.1:19000` **`via=relay`** | `w-47 → http://127.0.0.1:19100` `via=local` |
| relay `/status` | `dial=0 dialDenied=0 dialFailed=0 statusHits=14 subs=1 pushed=2 snaps=2`|`endpoints=[{port:19000,hostId:w-106,localPort:39525}]` |
| 47 上 `:21000` / `:19000` 监听 | **0 / 0** |
| `overlay-endpoint` 日志(全量历史) | **0 行** |
| 47 上 `dsh-*.scope` | **0**(⚠️ guest 实例在 **106** 上 ⇒ 见 ④) |
#### ② 四条判据(E2E 主体 · 全部通过)
手段 = **R4** 临时 session:`node /opt/dshs/mksess-guest.cjs` ⇒ `POST http://127.0.0.1:3080/api/dsh/enter`(`--http1.1`,**同一条连接连发两次**)。
| # | 判据 | 期望 | 实测 | 判定 |
|---|---|---|---|---|
| ⓐ | `enter` HTTP 码 | **200** | `…\|200` + `…\|200`(同连接双发)+ 复测 6 轮**全 200** | ✅ |
| ⓑ | 47(Manager)上 `ss -lntp \| grep -c ':21000'` | **0** | **0**(`:19000` 亦为 **0**) | ✅ |
| ⓒ | `journalctl -u dshs \| grep -c overlay-endpoint` | **0** | **0**(窗口内 delta = **0**、全量历史 = **0**) | ✅ |
| ⓓ | relay `/status` `counters.dial` 增量 | **> 0** | **0 → 2**(`denied=0` / `failed=0`) | ✅ |
`enter` 响应体原文(截断):
```
{"kind":"session","instance":{"id":"b5e5b0cd-…","port":21000,"status":"running","launchToken":"dVQHA02lJYqOE8UyRJzJoTuOmpd0MBd9oMkoUzCg9Js"},"url":"https://guest.alotbuy.com/?token=…"}
```
#### ③ 判别器证据(**比 `dial` 计数更强的一条**)—— 两条链**各自**都拨到了
```
20:10:59 [relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000 ← agent 面
20:10:59 [relay-client] dial w-106:19000 up (stream=2)
20:13:02 [relay-dialer] 落点 127.0.0.1:25001 -> ops/w-106:21000 ← 实例面
20:13:03/04 [relay-client] dial w-106:21000 up (stream=3/4/5)
```
🔑 **为什么这条是 P-2 / P-2b 的直接验收**:`dsh_hosts` 里 w-106 的 `endpoint` 是 `http://127.0.0.1:19000`(**worker 侧口号**)。拨号日志里出现的是**逻辑名 `ops/w-106:<口号>`**(`19000` = agent 面、`21000` = 实例面),且 **47 本机 `:21000` / `:19000` 监听恒为 0** ⇒ 说明:① `hostId → 逻辑名` 的键口径修对了(**P-2**)② 拨的是 relay 回环落点、⛔ 不是 worker 侧口号 ③ 实例面那条走的就是 **P-2b** 新增的候选路径。同刻 `/status` 出现两条端点:`{port:19000,localPort:39525}` + `{port:21000,localPort:43431}`。
#### ④ 实例真身在 106(旁证)+ 用户可见面 200
- **106 侧**:`dsh-100002-ef8d1d12.scope` = **active running**,argv 末尾 = `dsh --profile web --host 127.0.0.1 --port 21000` ⇒ `127.0.0.1:21000` **在 106 上监听**(`count_21000=1`),agent 口 `19000` 亦在听;`dshs-worker` / `dshs-relay` = active。
⇒ **"47 上 `:21000` = 0" 与 "106 上有实例在听 21000" 同时成立** = 翻译生效的**强判据**(若翻译失效,Manager 会去连 **47 自己的** `127.0.0.1:21000` ⇒ 连接被拒 ⇒ 正是本线 2026-09-16 踩过的"空响应 + 零日志")。
- **用户可见面(补齐 `OBS-09` 的语义)**:`curl -H 'Host: guest.alotbuy.com' http://127.0.0.1:3080/` ⇒ **200**、**62451** 字节真 HTML(`<!doctype html> … __ModuleLoader__`)、`502/Bad Gateway` 标记 **0** ⇒ 序⑬ **Q2**(guest(w-106) 实例页 502)的判据**现已满足**。
#### ⑤ 零回归三件套(全绿;均带 `--table`)
| 项 | 结果 | 基线 | 判定 |
|---|---|---|---|
| `npm test`(Node 22) | **176 tests / 175 pass / 0 fail / 1 skip** | 176/175/0/1 | ✅ 零回归 |
| `--scene all --table` | **12 PASS / 0 SKIP / 0 FAIL**,`rc=0`(幕1-A 17494ms/幕4-A 22234ms + 一跳豁免 17996ms) | 12/0/0 | ✅ 零回归 |
| `overlay-probe --table` | **16 PASS / 0 FAIL / 0 SKIP**,`rc=0` | 15/0/1 | ✅ **+1(better)** |
🔴 **`overlay-probe` 由 `15/0/1` → `16/0/0` 的原因 = `OBS-09` 由 SKIP 转 PASS**:
```
PASS OBS-09 在册实例面 w-106:37747=401 (阈值 ∈ {200,401})|不在册 1 项(记 SKIP 的依据):本机:20000
```
⇒ 这**正是序⑲ §9-8 写下的回头条件**("任一用户访问自己的实例页后重跑探针 ⇒ `OBS-09` 应回 PASS")**已兑现**,且**不是**真回归。其余 15 项同基线(`OBS-11` 实际 77 多出 0 缺失 0 = 端点数 1→2 各占一个回环落点,集合判据已绿;`OBS-15` p95 = **1770 ms** ≪ 15000 ms)。
**收口归零**:47 `dshs`/`dshs-relay`/`dshs-pg` active、门户 **200**、池口 **64**、`:21000` 监听 **0**|106 `dshs-worker`/`dshs-relay` active。
#### ⑥ 边界自证 + 指纹
⛔ **未改任何代码 / 未改任何生产值 / 未动 nft·nginx / 未新增公网监听口** —— 本棒**只做只读取证 + 临时 session**。逐项:
| 项 | 期望 | 实测 |
|---|---|---|
| 参数表指纹(`sed '/^## §10 指纹/,$d' … \| md5sum`) | `42238175d84319ada99afa56d583f9db`(未变) | **`42238175d84319ada99afa56d583f9db`** ✅ |
| 本单 §8 前缀 | `f612858344077420cff5f1f9ef1c942f`(未变) | **`f612858344077420cff5f1f9ef1c942f`** ✅ |
| in-册代码面 md5(`endpoint-target.ts` / `web/server.ts` / `relay.test.mjs`) | 与序㉒ 同值 | `9f421a9f…` / `8c4c072c…` / `f08088b8…` ✅ **全同** |
| 代码仓 HEAD / 改动数 | `04776af` / 10 | **`04776af` / 10** ✅ |
| 🔴 `COOLDOWN_MS=0` 命中(`scripts`+`src`+`test`) | **0** | **0** ✅ |
| R4 临时 session 残留 | **0** | **0** ✅(两轮用完即删,`user_agent='poc-curl2'`) |
| ⛔ commit / push | 无 | **无** ✅ |
#### ⑦ 收口复核结论(本棒第二件)—— **线内可停**
| 项 | 状态 |
|---|---|
| **P-1**(presence 新鲜度判据) | ✅ 已修(§8.11-①;长窗复核见 §8.12-②) |
| **P-2**(`translateEndpoint` 键口径) | ✅ 已修 + **E2E 验收通过**(本节 ②③) |
| **P-2b**(候选链两级 → 三级) | ✅ 已修 + **E2E 验收通过**(本节 ②③) |
| **E2E 缺口**(§8.11-⑤) | ✅ **已补齐**(本节 ②③④) |
| **在册缺陷表** | **空**(P-1 / P-2 / P-2b 全关闭) |
| §9 第 10 条(relay 静默丢订阅) | 回头条件(当前代码路径**不可达**)⇒ 非在册缺陷、无需动作 |
| 清单 §五 第 7 步剩余(内容分发 · 房间层 · 游戏) | **全部待拍板** |
| 入口 §4(骨干节点服务范围 A/B) | **待拍板** |
| 「Manager 重启后是否自动拉起既有实例」 | **待拍板** |
⇒ **覆盖网络线(presence 部分)线内可停**:在册缺陷清空、E2E 补齐、剩余项**全部落在「需拍板」区**。按**登记门禁**(用户 2026-09-17 明令:待拍板未定 ⇒ 不登记下一棒)**本棒不登记接续**,等拍板后再建。
#### ⑧ 本轮新登记(⛔ R7:只报告、未动手)
- **O-1(线外 · 集群化/模型落地面)**:`enter` 时 47 日志出现 `model landing failed, falling back to env injection Error: ENOENT … open '/var/lib/dshs/users/4092b965-…/home/.credentials.yaml'` —— 即**用本地路径读远端实例 home**(与序⑬ Q2 所记真因**同族**)。**影响**:`landModels` 退化为 env 注入(有兜底、**用户可见面仍 200**);**归属**:`RemoteSpawner.resolveApiKey`(平台模型落地),**不在本线 §3.1 文件集** ⇒ 仅登记。
- **O-2(观察项)**:`OBS-15` p95 本棒 = **1770 ms**(序㉒ 曾见 14220 ms,阈值 15000 ms)⇒ 抖动主导、**未越界、⛔ 未改阈值**。
**登记**:本棒**不登记下一棒**(线内可停 + 剩余项全为拍板项);下一棒的触发条件 = 用户对 **B(内容分发)/「Manager 重启后自动拉起实例」/入口 §4 骨干服务范围**中的任一项拍板。
**回填后指纹自检**:§8.13 位于 §8 之后 ⇒ **§8 前缀指纹 `f612858344077420cff5f1f9ef1c942f` 应保持不变**。
---
## §9 回头条件(**一出现必须回头**;⛔ 不许自行扩范围、⛔ 不许调生产值去凑)
1. **要动生产值才能继续** —— 需要改 `RELAY_FAILOVER_*` / `HB_SEC` / burst 任一 ⇒ **停下报告**(D1)。
2. **要扩大暴露面** —— 需新增公网监听口 / 放开遮蔽 / 跨网络或跨租户订阅 ⇒ **命中 R5** ⇒ 停下出权限评估。
3. **要超出 §3.1 的文件集** —— 除参数表 / 测试 / 探针 / 文档外,改动溢出到 `src/**` 的其他层 ⇒ **停下报告**,别扩大。
4. **批合并失效** —— 一次状态变化推不出 ≤1 帧,或稳态出现非零 presence 帧 ⇒ **停下报告**(D4 的前提被证伪)。
5. **观测跑不出单值** —— `OBS-13/14/15` 在真机拿不到可判定的数字 ⇒ **停下报告**,改口径(⛔ 不许把未验证的项标 PASS)。
6. **非静态失败** —— 跨网络订阅被**静默放行**(而非显式拒绝),或未知帧号被**静默丢弃** ⇒ **立即停**(假绿,本线头号教训)。
7. **要求"先有房间层才能验收"** ⇒ **停下报告**:本单口径是**节点层单值判据**,房间层不在范围内。
8. **零回归退化** —— `npm test` 低于 **162/161/0/1**、或 `--scene all` 出现 FAIL、或探针低于 **12/12** ⇒ **停下报告**。
9. 🔴 **任何回滚 / 演练 / 夹具路径要写 `RELAY_FAILOVER_COOLDOWN_MS=0`** ⇒ **禁止**(不是"回头",是禁令)。
10. 🆕 **序㉑(P-1 修复)新增**:**relay 侧"静默丢订阅、socket 仍活"** ⇒ 门的判据(订阅已建立 ∧ 链路活着)**察觉不到**,镜像会长期不动而没人知道。当前代码里这条路径**不可达**(唯一清空 `session.subs` 的是显式 `UNSUB`;relay 重启 / 会话回收都会断 socket)。**一旦出现(现象 = 订阅在册却再也收不到任何 `PRESENCE` 帧,且 `subs` gauge 与客户端 `state` 不一致)** ⇒ 正解 = **心跳帧携带订阅态断言**,或**周期性 `SNAP` 复核**;⛔ 不许靠"缩短 TTL"(那只是把 P-1 反向重犯)。
---
## §10 指纹与状态
| 项 | 值 |
|---|---|
| 本单 §8 前缀指纹 | **`f612858344077420cff5f1f9ef1c942f`**(落单时算出,2026-09-17 17:2x;口径 = `sed '/^## §8 回报格式/,$d' … \| md5sum`) |
| 参数表指纹(改前) | `8f08e74b026e6e5b5e1b3db813f031ae`(序⑰ 收口值,2026-09-17 17:2x) |
| 代码仓 HEAD(落单时) | `bc0dd2c`(已 push,远端 `master` 一致) |
| 本单状态 | **已执行完毕(线内可停)**:序⑲(落地 + 真机验收)→ 序⑳(`OBS-09` 口径修正 + 收益量化 ⇒ **P-1 在册**)→ 序㉑(P-1 + P-2 均已修,§8.11)→ 序㉒(P-2b 已修 + P-1 长窗复测通过,§8.12)→ **序㉓(P-2 / P-2b 的 E2E 验收通过 + 在册缺陷表清空 + 线内可停,§8.13)** ⇒ 剩余 = §9 第 10 条(回头条件)+ **三项待拍板**(`B` 内容分发 / 「Manager 重启后自动拉起实例」/ 入口 §4 骨干服务范围) |
| 参数表指纹(现行) | **`42238175d84319ada99afa56d583f9db`**(序㉑/序㉒/序㉓ 收口值;**序㉒ 与 序㉓ 均未变** —— 两棒无数值改动 ⇒ 同时是"零生产值改动"的自证;前值链 `659c08949c193102f774822905fde3b7` ← `13de5f9b77c486d71e5b83ec909b17b2` ← `8f08e74b026e6e5b5e1b3db813f031ae`) |
| 本棒边界自证 | ⛔ 未改码 / ⛔ 未动 47·106 / ⛔ 未 commit·push / ⛔ 未改任何生产值 / 🔴 `COOLDOWN_MS=0` 仅以"禁令"形式出现 |
---
## §11 规划棒回报(序 ⑱ · 2026-09-17 17:22–17:2x)
**做了什么**:只出规划 —— 产出本单(**287 行 / 纯 LF**),⛔ 零代码改动、⛔ 零服务器改动、⛔ 零 commit / push、⛔ 未改任何生产值。
**本棒实测三条证据**(§0.1 原文):`grep -rilE "\b(room|chat)\b" src | wc -l` = **0**(零应用层)|`grep -ril presence src` = **仅 `src/net/relay/placement.ts`**(第 21 行是注释)|`grep -n "req.url === '/status'" src/net/relay/server.ts` = **413** 且 **1234** 行原文写明「`/status` 是 Manager …」⇒ **现役在线态 = 拉快照(要替换的那条路)**。
**本棒的核心判断**:入口 §2 旧结论「presence 无从下手」**要收窄** —— 正确表述是「**应用层 presence 无从下手、节点 presence 正当时**」:序 ①–⑰ 已把它全部前置做完,且落地载体(`src/net/relay/**` 共 15 个模块)**今天就存在**。
**判据与边界**:
- 单内 **§4.2 真取舍 = 空**、**§4.3 待拍板 = 空**(三个曾考虑的项均有客观判据 ⇒ 自决拍掉);**§4.4 R5 评估 = 未命中**(新增监听口 / 凭据 / 入站均 **0**,可见面只收窄)。
- ⛔ 未规划 **B(内容分发 / 块级内容寻址)** —— 仍待拍板,只登记一行(§0.4-1 / §3.3-1)。
- ⛔ 未为生产值留任何改动口;**§9-9 把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写成硬禁令**(不是"回头条件")。
**登记**:下一棒 = **序 ⑲ 执行棒**,automation **`107b8e38-ccbd-4dcf-8b49-25dad7c89be9`**(名称「覆盖网络线-序19执行棒-presence节点在线态」,**一次性**,`scheduledAt` = **2026-09-17 17:28** = 收口 +4 min,`nextRunAt` = 1789637280000)。
**回填后指纹自检**:§11 位于 §8 之后 ⇒ **§8 前缀指纹 `f612858344077420cff5f1f9ef1c942f` 应保持不变**(口径 = `sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum`)。