Files
dsh_ai1net_server/归档/交接单-20260924-归档/交接单_实例逐步拉起_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

264 lines
22 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.
# 交接单 · Manager 重启后「逐步拉起」既有实例
- **序号**:覆盖网络线 **序 ㉕ · 规划棒**
- **立单**:2026-09-17 20:3x
- **用户拍板**:**「Manager 重启后是否自动拉起既有实例:逐步拉起」**(2026-09-17 20:2x)
- **上游依据**:档案 **30**(`dsh-server-docs/04-调整方案/30-编排器孤儿实例清理.md`)/序⑲ §8.8-1("按需拉起、无 reconciler"的实测)/序⑬ P3(`OBS-09` 因无实例而 SKIP)
- **状态**:**待执行**
---
## §0 摘要 + ⚠️ 一条必须先说的反直觉事实
用户拍板 = **"逐步拉起"**(staggered rehydrate)。但**读源码发现:现在的架构与目标不是"缺一个 reconciler",而是"启动时主动清空"** ——
| 位置 | 现状 | 与目标的差距 |
|---|---|---|
| `src/supervisor/orchestrator.ts:208`(构造函数) | `this.cleanAllStaleScopes()` —— **portal 启动即清掉全部实例 scope** | 🔴 **这是"逐步拉起"的直接阻碍**:重启后 **N=0**,没有任何可接管的实例 |
| `:1225` | `stopScopesByPrefix('dsh-', /^dsh-\d+-[0-9a-f]+\.scope$/)` | 清得**很干净**(连"哪些曾经在线"的信息一起丢了) |
| `:1218` | `cleanStaleScopes(uid)` —— spawn 前清**同 uid** 名下的残留 scope | ⚠️ 这一条是**必要的**(档案 30 的根因:双实例共 profile ⇒ 写冲突)⇒ **⛔ 不许为了"逐步拉起"把它去掉** |
**根因链(档案 30 原文)**:实例真实生命周期在 **OS 层(systemd scope)**,而编排器**只靠内存 map 追踪** ⇒ 重启后 map 空 ⇒ 旧 scope 变**孤儿**(编排器不认识)⇒ 双实例共享 profile ⇒ session/settings 写冲突 ⇒ "模型连接异常"。当时的修法是"**统一清掉防孤儿**"。
⇒ **本单的核心判断**:**"逐步拉起"= 把"清空"换成"接管"** —— 启动时**不要清**,而是**扫描 OS 层既有 scope ⇒ 逐个回填进内存 map ⇒ 按节流逐个 probe/rehydrate**。⚠️ **前提是先有"可恢复的实例账本"**(否则"清"是唯一安全的做法)。
---
## §1 目标(可判定"做完了没有")
| # | 判据 | 期望 | 怎么测 |
|---|---|---|---|
| **E1** | 重启后实例**不被清掉** | `restart dshs` 后 5 s 内,重启前在跑的 scope **仍在** | 先起 2 个实例 ⇒ `restart dshs` ⇒ `systemctl list-units 'dsh-*.scope'` **仍为 2** |
| **E2** | **逐个**(节流)接管,⛔ 不风暴 | 接管速率受限(`REHYDRATE_CONCURRENCY` / `REHYDRATE_STAGGER_MS` 阈值内) | 4 个实例 ⇒ 日志逐条时间差 ≥ 阈值;⛔ 无"同一秒 4 条" |
| **E3** | 任一实例都是**原实例**(不是新起) | 接管后 `port` 不变、无新 scope 产生 | 对比重启前后的 scope 名与 `--port` 参数 |
| **E4** | 单实例保证**仍成立**(⛔ 不回归档案 30) | 接管后同 uid **只有 1 个** scope;新 spawn 前仍清同 uid 残留 | 断言 `dsh-<uid>-*` 计数 = 1 |
| **E5** | 无实例时**不自起**(⛔ 不违反"按需"语义) | 重启前**没有**实例 ⇒ 重启后仍为 0 | 空跑一次 `restart dshs` ⇒ scope = 0 |
| **E6** | 内存/启动压力可控 | 接管过程 47 可用内存不跌穿阈值;无 OOM kill | `free` 采样 + `journalctl -u dshs \| grep -c 'oom'` = 0 |
| **E7** | 零回归 | `npm test` **176/175/0/1**、`--scene all --table` **12P/0S/0F**、`overlay-probe --table` **16P/0F/0S** | 三件套跑同值 |
---
## §2 只读前置(执行前**必须**先核实的 4 条)
| # | 要核实什么 | 命令 | 期望 |
|---|---|---|---|
| **P1** | 实例 scope 与 uid 的对应 | `systemctl list-units 'dsh-*.scope' --plain` + 对照 `users` 表 | 确认 `dsh-<uid>-<id>.scope` 里 **`<uid>` = 用户 uid**(档案 30 口径),⛔ 别与"用户 id"混 |
| **P2** | scope 的 `--port` 参数**重启后是否可读回** | `cat /proc/<pid>/cmdline` 或 `systemctl show -p MainPID` 后读 `/proc` | ⚠️ **这是可恢复性的关键**:拿不到端口 ⇒ 没法接管(要另想办法) |
| **P3** | 47 上当前实例数(T0 现场) | `systemctl list-units 'dsh-*.scope'` | 序㉓ 实测 47 = **0**、106 = **1**(⚠️ 实例在 **106** 上)⇒ 本单验收要**主动先起实例** |
| **P4** | 是否有"实例账本"可持久化 | 查 `RemoteSpawner` / `LeasedSpawner` 落库情况 | 若既无 DB 记录也无 /proc 可读 ⇒ **必须先补一步"实例账本"**(见 §4 S1 前置) |
---
## §3 范围
### 3.1 在册文件集(⚠️ **超出必须先停下报告** — R7)
| 面 | 文件 | 说明 |
|---|---|---|
| 改动 | `src/supervisor/orchestrator.ts` | ①② 启动流程:清空 → 接管;新增 `rehydrate()` |
| 改动 | `参数表_覆盖网络_20260917.md` | 新增 `REHYDRATE_*` 键 |
| 改动 | `scripts/overlay-probe.cjs` | 新增观测项(重启后 scope 数 = 重启前) |
| 可能新增 | `test/orchestrator-rehydrate.test.mjs` | 单测(⛔ 不改 `package.json` test 列表的结构) |
⛔ **不在本单范围**:改 bwrap 参数(🔴 **47 是 bubblewrap 0.4.0**,`--perms` 属 0.5+ ⇒ 会起不来)、改 `cleanStaleScopes(uid)` 的"spawn 前清同 uid"语义、动 `RELAY_FAILOVER_*` / `HB_SEC` / burst。
### 3.2 三条硬约束(**违反即事故**)
1. 🔴 **`cleanStaleScopes(uid)`(spawn 前清同 uid)⛔ 不许删** —— 它是档案 30 的修法本体,删了就回到"双实例共 profile"。
2. 🔴 **`cleanAllStaleScopes()` 只能改成"接管",⛔ 不能改成"什么都不做"** —— 因为内存 map 为空时,**孤儿会失控**(这正是档案 30 的原始故障)。
3. 🔴 **接管必须"认原实例"** —— ⛔ 不许"重启后重新 spawn 一遍"(那会**换端口、换 scope 名**,且瞬间 N 个一起起 = 启动风暴)。
---
## §4 步骤 S0–S6
| 步 | 做什么 | 验证(一次可执行) | 回滚 |
|---|---|---|---|
| **S0** | 只读前置 `P1–P4`(**零改动**) | 拿到"scope 名 ↔ uid ↔ 端口"三者的可读性结论 | 无(只读) |
| **S1** | **(可能需要)实例账本**:把"在线实例"持久化(⚠️ 归属判定:跟着用户走 ⇒ **放实例 home**;本机运维用 ⇒ **Worker 本地库**;⛔ 只有 Manager 能写归属/租约) | 重启后能列出"重启前在跑的实例"清单(含 uid / 端口) | 删账本文件 |
| **S2** | **接管取代清空**:启动扫描 → 回填 map(⛔ 不 stop、⛔ 不 spawn) | `E1`:`restart dshs` 后 scope 数**不变** | 还原 `cleanAllStaleScopes()` 调用 |
| **S3** | **节流**:`REHYDRATE_CONCURRENCY` / `REHYDRATE_STAGGER_MS` | `E2`:4 实例 ⇒ 日志时间差 ≥ 阈值、⛔ 无同秒多条 | 阈值调回 0 ⇒ 回 S2 行为 |
| **S4** | **单实例保证复验**(⛔ 不回归档案 30) | `E4`:同 uid 恒 1 个 scope;`E5`:无实例时不自起 | 同 S2 |
| **S5** | 观测 + 探针新项 | 探针出一项 PASS/FAIL(⛔ 阈值不许是脚本魔数) | 还原探针 |
| **S6** | 真机验收(`E1`–`E6`)+ 零回归(`E7`)+ 收口 | `E6`:内存不跌穿、无 OOM;三件套同值 | 产物回滚点 |
---
## §5 真机验收怎么造条件的(⚠️ 关键)
⚠️ **不能只靠"等用户访问"**(本线已因"无 reconciler ⇒ 不自回"卡过一次)。验收必须:
1. **先主动起 2–4 个实例**(用 R4 临时 session 走真实 `enter`,或用本机多实例)⇒ 记录 scope 名 / uid / 端口;
2. `systemctl restart dshs`;
3. **立刻**(≤ 5 s)与 **+60 s** 两个时刻各取一次 `list-units 'dsh-*.scope'`;
4. 断言 `E1`(数不变)/`E2`(逐条时间差)/`E3`(端口不变、⛔ 无新 scope)/`E4`(同 uid = 1)。
⚠️ **已知副作用**:`restart dshs` **会收掉用户实例 scope**(本线既有实测)⇒ 本单要**故意利用**这个副作用做 E1/E5 的对照(重启前有实例 → 应被接管;重启前无实例 → 应保持 0)。
---
## §6 回滚(两层)
1. **代码级**:还原 `orchestrator.ts` 的启动流程为原 `cleanAllStaleScopes()` 调用 → `tsc` → scp `lib/` → `restart dshs` ⇒ 回到"统一清掉防孤儿"(档案 30 行为)。
2. **产物级**:scp 回滚点 `/opt/dsh/backups/seq25-<ts>/lib/supervisor/orchestrator.js`。
🔴 ⛔ **回滚路径里不许出现 `RELAY_FAILOVER_COOLDOWN_MS=0`**。
---
## §7 回报格式(执行会话**必须**回填)
```markdown
### 8.x 序 ㉕ 执行棒回报(YYYY-MM-DD HH:MM–HH:MM)
#### ① S0–S6 逐条(命令 + 原文输出 + 判定)
#### ② 主判据 E1:重启后实例数不变
- 重启前 scope = ? | +5 s = ? | +60 s = ? | 端口是否全同 = ?
#### ③ 先红后绿(原文级)
- (先跑"仍清空"的旧行为 ⇒ 哪条断言红;改成接管 ⇒ 绿)
#### ④ 零回归三件套(均带 `--table`)
#### ⑤ 边界自证
⛔ 未改生产值 / ⛔ 未改 bwrap 参数 / ⛔ 未删 `cleanStaleScopes(uid)` / 🔴 `COOLDOWN_MS=0` = ? / ⛔ 未 commit·未 push
#### ⑥ 指纹
参数表 = ?(前值 `42238175d84319ada99afa56d583f9db`)|orchestrator.ts md5 = ?
```
---
## §8 回头条件(**一出现必须回头**)
1. `P2` 拿不到 scope 的端口 ⇒ **停下报告**("接管"不可行,需先出"实例账本"方案)。
2. 接管导致**双实例共 profile**(档案 30 故障复现)⇒ 立刻回滚 + 报告。
3. 需要**超出 §3.1 文件集** ⇒ 停下报告(R7)。
4. 要**改 bwrap 参数 / 改配额 / 动生产值** ⇒ 停下报告。
5. `E2` 跑不出节流(同秒多条)⇒ 停下报告,⛔ 不许放宽判据。
6. 零回归三件套任一退化 ⇒ 停下报告。
7. 🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**。
---
## §9 未验证项
| # | 项 | 状态 |
|---|---|---|
| 1 | 重启后能否从 OS 层读回实例端口 | ⚠️ 待 `P2` 取证 |
| 2 | 是否需要独立"实例账本" | ⚠️ 待 `P4` 取证 |
| 3 | 接管 4 个实例的内存与启动耗时 | ⚠️ 待 S6 实测 |
---
## §10 指纹与状态
| 项 | 值 |
|---|---|
| 本单 §7 之前正文前缀指纹 | **`d9d48121e68faa00e1da17ad8fea36ad`**(口径 = `sed '/^## §7 /,$d' 交接单_实例逐步拉起_20260917.md \| md5sum`) |
| 参数表指纹(立单时) | **`42238175d84319ada99afa56d583f9db`** |
| 代码仓 HEAD(立单时) | `04776af` |
| 本单全文件 md5(立单时) | `2c53a018c2d9bfc2c422d0758413a485`(161 行) |
| 本单状态 | **已执行(2026-09-17 22:05–22:2x)** —— ✅ **认领逻辑已落地并在真机夹具上实测工作**;🔴 **但主判据 E1 在"真实实例"上未达成**,根因 = 单子未识别的**第二条杀实例路径(退出时 `teardown()`)**。详见 §11。⏸ 本序**未登记续棒**(需你拍板是否扩大文件集)。 |
---
## §11 序 ㉕ 执行棒回报(2026-09-17 22:05–22:2x)
### 8.1 六段回报
#### ① S0–S6 逐条
| 步 | 结果 | 命令 / 证据 |
|---|---|---|
| **S0** 只读前置 P1–P4 | ✅ | **P1** `dsh-100002-ef8d1d12.scope` ⇒ scope 名第 2 段 = **uid**(`100002`)✅。**P2** ⚠️ `systemctl show -p MainPID --value` **为空**,但 `-p Description` **携带完整 argv**(末尾 `--port 21000`、`--reuid 100002`、`--chdir …/users/4092b965-…/ws`)⇒ **端口可读回,通道是 `Description` 不是 `/proc`**;且解析可用 **三道自洽校验**(`--profile` 必有、`--chdir` 含 `/users/<uuid>/`、`--reuid` == scope 名 uid)⇒ **§8 回头条件 1 未命中**。**P3** T0 现场:47 `dsh-*.scope` = **0**、106 = **1**(实例在 106)✅ 与序㉓ 同。**P4** 🔴 `dsh_instances` 表**全仓零调用方**(`src/db/pg.ts` 定义了 `upsertInstance/getInstanceByUser/…`,但 `grep -rn` 在 `src/db/` 外 **0 命中**)= **k8s 形态预留的死代码** ⇒ **无现成账本** ⇒ 采纳「**以 OS 层 scope 为唯一权威**」,⛔ 不新建账本(新建 = 引入双写与陈旧风险,R11)。 |
| **S1** 实例账本 | ✅ **判为不需要** | 由 P4 得出:所需四要素(uid / userId / port / folder)**全部可从 scope 名 + Description 解出** ⇒ 加账本是净负担。 |
| **S2** 接管取代清空 | ✅ 代码已落 / 🔴 真机**部分**达成 | 构造函数由 `cleanAllStaleScopes()` 改为 `rehydrateAdoptedScopes()`;新增 `scanExistingScopes`/`scopeDescription`/`adoptOne`/`probeAdopted`/`stopUnit`;`cleanAllStaleScopes()` **保留**(非 account 回退 + 回滚路径)。 |
| **S3** 节流 | ✅ | 逐条经 `setTimeout` 排队,间隔 `DSHS_REHYDRATE_STAGGER_MS`(缺省 500 ms);真机日志逐条时间差 ≥ 阈值,⛔ 无同秒多条。 |
| **S4** 单实例保证复验 | ✅ | 同 uid 多 scope ⇒ `decideScopeAction` 判 `dup-uid` ⇒ **全部停**;`cleanStaleScopes(uid)`(spawn 前清同 uid)**一字未动** ⇒ 档案 30 未回归。 |
| **S5** 观测 + 探针新项 | ✅ | 参数表新增 `DSHS_REHYDRATE_STAGGER_MS` / `DSHS_REHYDRATE_PROBE_MS` + §6 `OBS-18`;探针新增 `OBS-18` + `--rehydrate-fixture`(三态夹具实证见 ③)。 |
| **S6** 真机验收 + 零回归 | ⚠️ **部分** | 零回归 ✅(见 ④);`E1` **夹具 ✅ / 真实实例 ❌**(见 ②)。 |
#### ② 主判据 E1:重启后实例数不变
- **夹具 scope(真实 systemd scope + 真实 argv 形态 + 真监听 21500)**:重启前 `dsh-100002-abcdef01.scope` = 1 ⇒ `restart dshs-worker` ⇒ **+6 s 仍 `is-active=active`、21500 仍在听、count = 1** ⇒ 🎉 **E1 ✅**(**证明 systemd 不会带走 scope、认领逻辑确实工作**)。
- **真实实例(`dsh-100002-ef8d1d12.scope`)**:重启前 count = **1** ⇒ `restart dshs-worker` ⇒ **+5 s count = 0** ⇒ 🔴 **E1 ❌**。
- 🔑 **根因(本次最重要的发现)**:`journalctl -u dshs-worker` 同一秒内 `Stopping … → Stopped …` + 新进程打印 `[rehydrate] 无既有实例 scope ⇒ 不动作` ⇒ **scope 在新进程扫描之前就没了**。真凶 = **`src/supervisor/orchestrator.ts:604` `teardown()`**:
```
async teardown(): Promise<void> {
if (this.reapTimer !== undefined) clearInterval(this.reapTimer)
for (const userId of [...this.mains.keys(), ...this.watchdogs.keys()]) await this.stop(userId)
}
```
由 **`src/worker/agent.ts:569`**(SIGTERM 关闭链)调用 ⇒ **退出路径主动停掉所有在册实例**。「原有实例」**不在** `mains` ⇒ `teardown` 碰不到它 ⇒ 它能活;**真实实例在** `mains` ⇒ 被杀。
- 🔴 **结论:本序只改"启动时清空"不足** —— 杀实例的**实际生效路径**是**退出时的 `teardown()`**(**同机** `LocalSpawner.teardown` + **跨机** `RemoteSpawner.teardown`,后者正是序⑲ 观测到"`restart dshs` 会收掉 `dsh-*.scope`"的原因)。
- ⛔ **未动 `teardown()`** —— 它需要改 `src/supervisor/remote-spawner.ts` / `leased-spawner.ts`(**不在 §3.1 在册文件集**)⇒ 按 **§8 回头条件 3**(R7)**停下报告**,见文末待拍板项。
#### ③ 先红后绿(原文级)
- **单测面**(`test/orchestrator-rehydrate.test.mjs`,**刻意不进 `npm test`** 以保基线可比):批量破坏三处 ⇒ **破坏后 3 红(正是预期的 R7 uid 交叉校验 / R13 同 uid 多 scope / R16 构造函数接线)**:
```
not ok 5 - R7 ⛔ uid 交叉校验不符 ⇒ 拒
not ok 11 - R13 ⛔ 同 uid 多 scope ⇒ 全部停
not ok 14 - R16 接线:构造函数必须调 rehydrateAdoptedScopes
# pass 15 / # fail 3
```
还原后 **`# pass 18 / # fail 0`**,且 `orchestrator.ts` md5 **与破坏前逐字一致**(`86f1f05f878d706cdc656e3d769800e5`)⇒ 断言链**有效且可自证**。
- **探针面(`OBS-18` 三态夹具,离线零副作用)**:旧行为原文(无 `[rehydrate]` 行)⇒ **FAIL**;合法 summary ⇒ **PASS**;`adopted+stopped ≠ scanned` ⇒ **FAIL 并点名**。
#### ④ 零回归三件套(均带 `--table`)
| 件 | 实测 | 基线 | 判定 |
|---|---|---|---|
| `npm test`(Node 22) | **176 / 175 pass / 0 fail / 1 skip** + `verify-inject` rc=0 | 176/175/0/1 | ✅ **逐字一致** |
| `--scene all --table` | **12 PASS / 0 SKIP / 0 FAIL** | 12P/0S/0F | ✅ 一致 |
| `overlay-probe --table` | **17 PASS / 0 FAIL / 1 SKIP**(18 项 = 基线 17 + 本序新增 `OBS-18`;`OBS-09` SKIP = "无实例在册"的**已知环境态**,⛔ 非回归) | 17P/0F/0S | ✅ 等价(`OBS-18` 由"不存在"→ **PASS**)|
⚠️ 中途一次 `overlay-probe` 出现 `OBS-08 FAIL`(端点表 2 条 / 离线 1 条)⇒ **重跑即回绿**(1 条 / 离线 0 条)⇒ 判为 `--scene all` **演练后的暂态**,⛔ 非本序引入(其数据源是 relay `/status`,与 `orchestrator` 无耦合)。✅ **如实留档**。
#### ⑤ 边界自证
⛔ 未改任何生产值(`DSHS_REHYDRATE_*` **只读进程环境取代码缺省**,47/106 的 `dshs.env` / `dshs-worker.env` **一字未写**)· ⛔ 未改 bwrap 参数 · ⛔ **未删** `cleanStaleScopes(uid)` · ⛔ 未动 `RELAY_FAILOVER_*` / `HB_SEC` / burst · 🔴 `COOLDOWN_MS=0` 计数 = **0** · ⛔ 未新增公网口 / 未改 nft·nginx · ⛔ **未 commit / 未 push** · ⛔ 认领路径**零凭据落盘**(单测 `R19` 钉住)· ⛔ 未改 `04-调整方案/**`。
**部署**:`lib/supervisor/orchestrator.js` scp 到 47 `/opt/dshs/lib` + 106 `/opt/dshs-cluster/lib`,两处 md5 全同 = **`5bfb15e00b592c293548ae17b639399b`**;回滚点 = **两机 `/opt/dsh/backups/seq25-20260917-220556/lib/supervisor/orchestrator.js`**(旧值 `fad7926494578258f86a91eac613a204`);已 `restart` 47 `dshs` + 106 `dshs-worker`(门户 200)。
#### ⑥ 指纹
- **本单 §7 之前前缀指纹** = `d9d48121e68faa00e1da17ad8fea36ad` —— ✅ **开工时校验通过**(依据未被改动)。⚠️ 回填 §11 之后**仍为同值**(回填全在 §7 之后,⛔ 不触碰 §7 之前正文)。
- **参数表指纹** = `54c854a5c09b561ea4920c44d8545cfe` → **`93e38d1a57fbcd3126d2122150a2e838`**(新增 2 键 + §6 `OBS-18`;口径 = `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum`)。
- **`src/supervisor/orchestrator.ts` md5** = `86f1f05f878d706cdc656e3d769800e5`(先红后绿还原校验后值)。
- **本单全文件 md5(回填 §11 后)** = `b73f2ec18fcf52055c4a24c3fcbdeb5b`(**242 行**)。
- **代码仓 HEAD** = `04776af`(⛔ 未 commit / 未 push)。
- **本序新增/改动文件**:`src/supervisor/orchestrator.ts`(✏️ 在册)|`scripts/overlay-probe.cjs`(✏️ 在册)|`参数表_覆盖网络_20260917.md`(✏️ 在册)|`test/orchestrator-rehydrate.test.mjs`(➕ 在册可新增)|⚠️ **无越界文件**。
### 8.2 本序沉淀的四条可复用事实
1. 🔴 **"杀实例"有两条独立路径,只改一条等于没改** —— 启动路径 `cleanAllStaleScopes()` 与**退出路径 `teardown()`**。判据:**用一个"不在 `mains` 里"的夹具 scope 做对照** —— 它活着就证明 systemd 无辜、真凶在应用层。(本次一步定位,省下大量猜测。)
2. 🔴 **`--scope` 的端口不在 `/proc`,在 `Description`** —— `systemctl show -p Description` 携带 `systemd-run` 记录的完整 argv。⛔ 别再按"读 `/proc/<MainPID>/cmdline`"设计。
3. 🔴 **"认领"不能把实例写回 `mains`** —— 那会让 `enter` 的复用分支去等一个**永不会出现**的 `launchToken`(`right dsh web` 只在启动时打印一次、不落盘、运行期取不出)⇒ 用户被 **503** 挡住 = **R11 净变差**。⇒ 正确的认领语义 = **只登记 + 探活**,实例由用户访问时的 `cleanStaleScopes(uid)` **自然替换**。⛔ 也**不许**把 token 落盘换"无感接管"(安全维度净变差,R11)。
4. ⚠️ **探针里 `ssh()` 的形参作用域是个坑** —— `sshPort` 定义在外层块,`OBS-18` 在另一个块里取不到 ⇒ `sshPort is not defined` 被 `catch` 吞成 `text=''` ⇒ 表现成"读不到日志"。⇒ 失败判别器**必须能区分"读不到"与"确无该行"**(本序已落:0 行时一并打出 `journalctl` 原文字节数)。
### 8.3 待拍板(**唯一一项**):E1 的缺口要不要打通
**问题**:`teardown()`(退出时主动停掉所有在册实例)是"重启后实例不被清掉"的**实际阻碍**,而它跨了三个文件 —— 其中 **两个不在本单 §3.1 在册文件集**(`src/supervisor/remote-spawner.ts`、`src/supervisor/leased-spawner.ts`)。按 §8 回头条件 3(R7)我**已停下、未动手**。
**候选 A —— 只改 `LocalSpawner.teardown()`(同机那一处)**
· 优点:文件集最小,1 个在册文件即可;同机形态(纯本机部署场景)能达成 E1。
· 缺点:**Manager 重启仍会跨机收掉 Worker 实例**(Manager 侧 `LeasedSpawner → RemoteSpawner.teardown()` 会向 Worker 下发停止)⇒ **你的诉求「Manager 重启后逐步拉起」依然不成立** ⇒ 基本等于白改。
**候选 B —— 三处一起改:`LocalSpawner.teardown()` + `RemoteSpawner.teardown()` + `LeasedSpawner.teardown()`**
· 优点:**唯一能真正达成用户诉求**的路径;语义自洽 —— 退出进程把存活实例留给下一个进程,回收责任完全交给「启动认领 + TCP 探活判孤儿」这一套(本序已实测工作)。
· 缺点:**超出本单 §3.1 文件集,需要你授权扩范围**;影响面 = **所有实例的生命周期语义**(实例不再随进程退出而回收)⇒ 必须接受"进程长期不重启时,实例由 idle-reap / 用户访问替换来收"这个前提。
**候选 C —— 用 env 开关把"退出不停实例"做成可灰度**
· 优点:能先在一台机打开、观察后再推开。
· 缺点:跨机那一处**照样要改**(开关落地点就在 `teardown` 里)⇒ 文件集不比 B 小;而"默认关"等于不生效、"默认开"等于 B ⇒ **没有增量价值,只多一个开关**,且多一个"忘开/忘关"的失效面。
**我的倾向 = B**(A 不达成目标,C 无增量价值)。⏸ 在你拍板前,本序**不登记续棒**,也不动那三个文件。