# DSH 机 /tmp 爆满 →「项目记忆丢失」诊断与处置

- 日期：2026-09-23
- 机器：DSH 机 192.168.31.76（本机）
- 触发：`/tmp` 曾爆满，DSH 无法执行任何命令；清空 /tmp 后，感觉「很多项目的记忆丢失」
- 结论（一句话）：**会话正文（真正的记忆）一条没丢；丢的是 `/tmp` 里的「大输出溢出缓存」和临时工作文件。根因是 DSH 的溢出目录默认落在 tmpfs 内存盘，需要把 `TMPDIR` 挪到磁盘并加清理/备份策略。**

---

## 一、现场实测事实

| 项目 | 实测值 |
|---|---|
| `/tmp` 挂载 | **tmpfs（内存盘）7.8G**，当前用 515M（7%）；重启即清空 |
| `/`（sda3） | 92G，已用 14G，**可用 74G** |
| `/home`（sda5） | 805G，已用 295G，**可用 470G** |
| DSH_HOME | `/home/zyw/.dsh`（距上次统计 547M） |
| 会话日志 | `~/.dsh/sessions/` 共 **187 个 `.jsonl.zstd`，zstd 完整性校验 187/187 全部通过，0 损坏** |
| 会话注册表 | `~/.dsh/storages/workspace.json` → 工作区 Downloads，已注册 **90 个会话**（与磁盘目录一一对应，无缺失） |
| 孤儿会话 | `/home/zyw` 工作区有 **6 个**老会话（8/29–9/3 的测试会话）未注册进 workspace.json，故 GUI 侧栏看不到 |
| 运行方式 | **pm2** 应用 `deepseek-harness`（id 0，`dsh web --no-open`，cwd `/home/zyw/Downloads`），由 systemd `pm2-zyw.service` 开机自启 |
| 进程环境 | **未设置 `TMPDIR`** → `os.tmpdir()` 回落到 `/tmp` |
| 当前 spill 根 | `/tmp/dsh-spill-22VzTS`（含 3 个大输出快照，仍在） |

### 损失清点（spill 指针失效范围）

| spill 根 | 状态 | 引用条数 |
|---|---|---|
| `dsh-spill-22VzTS` | 存在（个别文件已缺） | 5 引用失效 / 3 文件在 |
| `dsh-spill-dusiyr` | **已消失** | 7 |
| `dsh-spill-ZzPuwD` | **已消失** | 7 |
| `dsh-spill-r74pBE` / `rSP6MD` / `pXgUcP` / `BEFdrr` | **已消失** | 各 2–3 |
| `dsh-spill-2M3vdQ` / `QBrazY` | **已消失** | 各 1 |

- **22 个历史会话**至少有一条「大输出全文」指针失效（全文不可恢复，会话里的截断预览仍在）。
- 5 个会话仍指向存活的 spill 文件（能正常查看全文）。
- `/tmp` 当前占用前列：`dh` 225M、`q21` 190M、`ocr_test` 79M、若干 PDF/临时目录 —— 都是会话期间的**临时工作文件**，重启即消失。

---

## 二、机制：DSH 为什么会被 /tmp 拖死、又为什么「看起来失忆」

1. **溢出后端写 /tmp**
   `@deepseek-ai/dsh-spill-local` 用 `os.tmpdir()` 调 `mkdtempSync(join(tmpdir(), "dsh-spill-"))` 建立**每个服务进程一个**的私有 spill 根；会话文件落在 `spill根/session-<sha256(sessionId)前12位>/`。
2. **超长工具输出只把全文放进 /tmp**
   `@deepseek-ai/dsh-spill-policy`（`maxInlineBytes` 阈值）在单次工具输出超限时：把**全文写入 spill 文件**，会话日志里只保留**截断预览 + 指向该文件的提示/路径**。也就是说：**会话日志保存的是「记忆」，spill 文件保存的是「全文快照」。**
3. **/tmp 是 tmpfs，且满了就是 ENOSPC**
   tmpfs 由内存承载，**重启一定清空**；写满后 `open()`/`mkdir()` 直接失败 → DSH 的 spill 写入、子进程临时目录、Node 编译缓存等一起报错 → 表现为「DSH 任何命令都跑不动」。
4. **清空 /tmp 的副作用**
   会话日志（在 `~/.dsh/sessions`，磁盘上）完好，但所有 spill 全文快照连同指向它们的路径一起没了 → 回看旧会话时「查看完整输出」失效；再加上服务重启后**每次都会新建一个 spill 根**，旧根成为孤儿，看起来就像「很多项目的记忆没了」。
5. **放大因素**
   - 服务启停频繁（pm2 记录 `restart_time: 79`），每次新建 spill 根，旧根不自动清理；
   - 系统 `/usr/lib/tmpfiles.d/tmp.conf` 里 `/tmp` **只配置了 `D /tmp 1777`（开机重建），没有按时间/大小清理**，所以会一直堆积到爆。

---

## 三、处置方案

### A. 立即执行（零风险，不动服务）

```bash
# 1) 先把现状整体备份一份（含会话/注册表/配置）
tar czf ~/dsh-backups/dsh-backup-before-tmpfix-$(date +%Y%m%d-%H%M%S).tgz \
  -C /home/zyw .dsh/sessions .dsh/storages .dsh/settings.yaml .dsh/AGENTS.md 2>/dev/null

# 2) 清理滞留的孤儿 spill 根（先看再删）
ls -ld /tmp/dsh-spill-* 2>/dev/null
#   只删 3 天前的；存活进程正在用的 22VzTS 保留
find /tmp -maxdepth 1 -name 'dsh-spill-*' -mtime +3 -exec rm -rf {} + 2>/dev/null

# 3) 把 /tmp 里有价值的临时产物搬回磁盘（按需）
#    例： mkdir -p ~/Downloads/.dsh-work && mv /tmp/dh /tmp/q21 /tmp/ocr_test ~/Downloads/.dsh-work/
```

### B. 根治：把 DSH 的临时目录挪到磁盘（需重启 DSH Web 约 10 秒）

> 影响：重启 `deepseek-harness` 会中断当前会话所在进程。**会话已持久化，不会丢**，刷新 GUI 后从侧栏恢复即可。
> 收益：① 不再把 7.8G 内存盘塞爆（磁盘可用 470G）；② spill 全文快照按会话哈希落盘，**跨重启存活**，旧会话仍能看全文；③ 重启后 `TMPDIR` 生效。

```bash
# 1) 建磁盘临时目录
mkdir -p /home/zyw/.dsh-tmp && chmod 700 /home/zyw/.dsh-tmp

# 2) 写 pm2 ecosystem（显式声明 TMPDIR，便于长期维护）
cat > /home/zyw/dsh-ecosystem.config.cjs <<'EOF'
module.exports = {
  apps: [{
    name: 'deepseek-harness',
    script: '/home/zyw/.config/nvm/versions/node/v22.23.2/bin/dsh',
    args: ['web', '--no-open'],
    cwd: '/home/zyw/Downloads',
    interpreter: '/home/zyw/.config/nvm/versions/node/v22.23.2/bin/node',
    env: { TMPDIR: '/home/zyw/.dsh-tmp' },
    autorestart: true,
    merge_logs: true
  }]
};
EOF

# 3) 用新配置重建应用并持久化（保证 pm2-zyw.service 重启后仍生效）
export PATH=/home/zyw/.config/nvm/versions/node/v22.23.2/bin:$PATH
pm2 delete deepseek-harness
pm2 start /home/zyw/dsh-ecosystem.config.cjs
pm2 save

# 4) 验证：进程环境里应有 TMPDIR，且新的 spill 根出现在磁盘目录
pm2 env 0 | grep -i tmpdir
sleep 10; ss -tlnp | grep 3080
```

### C. 预防

```bash
# 1) 定时清理 spill（保留 14 天，兼顾“看全文”与磁盘）
( crontab -l 2>/dev/null; echo '30 4 * * * find /home/zyw/.dsh-tmp /tmp -maxdepth 1 -name "dsh-spill-*" -mtime +14 -exec rm -rf {} + 2>/dev/null' ) | crontab -

# 2) /tmp 兜底：按大小/时间清理（可选，写 /etc/tmpfiles.d/dsh-tmp.conf）
#    q /tmp/dsh-spill-* - - - 3d
```

- **习惯建议**：会话中的下载/中间产物一律放 `~/Downloads/.dsh-work/` 或 `dl-hub/` 子目录，**不要落在 `/tmp`**（`/tmp` 只当易失缓存）。
- **备份建议**：定期（每周）把 `~/.dsh/{sessions,storages,AGENTS.md,settings.yaml}` 打包到 `~/dsh-backups/`；现有可用备份：`~/dsh-backups/dsh-backup-pre-015rc2-20260919-231342.tgz`（441MB，含 9/19 23:13 前的全部会话）。

---

## 四、验收/复查命令

```bash
# 会话日志完整性（应输出 total=N corrupt=0）
cd ~/.dsh/sessions; bad=0; t=0; for f in $(find . -name '*.zstd'); do t=$((t+1)); zstd -t "$f" >/dev/null 2>&1 || { bad=$((bad+1)); echo "CORRUPT $f"; }; done; echo "total=$t corrupt=$bad"

# 失效 spill 指针统计（DEAD 计数 = 已丢失全文的引用数）
for f in $(find ~/.dsh/sessions -name '*.zstd'); do for r in $(zstd -dc "$f" 2>/dev/null | grep -oE '/tmp/dsh-spill-[A-Za-z0-9]+/' | sort -u); do root=$(echo "$r" | cut -d/ -f3); [ -e "/tmp/$root" ] && echo ALIVE || echo DEAD; done; done | sort | uniq -c

# TMPDIR 是否生效
pm2 env 0 | grep -i tmpdir; ls -ld /home/zyw/.dsh-tmp/dsh-spill-* 2>/dev/null
```

## 五、可恢复性说明

- **会话正文/标题/工具调用记录**：完好，可在 GUI 侧栏正常恢复。
- **超长工具输出的全文快照**（spill）：`/tmp` 已清，**不可恢复**；会话内仍保留截断预览。
- **若当时按规范把产出发布到了 `dl-hub/`**：正式交付物都在，不受影响。
- **9/19 之前的旧内容**：可从 `~/dsh-backups/dsh-backup-pre-015rc2-20260919-231342.tgz` 中按需找回。
- **6 个 `/home/zyw` 孤儿会话**：文件都在（`~/.dsh/sessions/--home-zyw--/`），需要的话可手工注册进 `workspace.json` 后即在 GUI 可见。
