docs(progress): log the orphan tmux session feature + the session_activity clock bug

This commit is contained in:
Yaojia Wang
2026-07-30 11:30:24 +02:00
parent 22a7929a7b
commit 2bfc76b397

View File

@@ -24,6 +24,25 @@
> 新会话读到的第一块。保持准确,只描述"此刻"。
### 🗂 [x] A · 让服务器"看不见"的 tmux session 重新可达(2026-07-30,worktree `orphan-tmux-sessions`)
- **起因**: 用户问"关掉桌面 App 会不会把 tmux session 一起关掉"。查证过程中发现一个**功能缺口**:`tmux ls`**69 个** `web_*` session,只有 **5 个**有 client —— 另外 64 个**从 UI 完全不可达**,其中一个还跑着 7 个 agent 的 Claude Code。最老的到 6 月 26 日。
- **根因**: `src/session/tmux.ts` 只有 `hasSession`,**没有 list**。`manager.list()` 只遍历内存表,`reapIdle` 也只遍历内存表。所以一个"活过了创建它的进程"的 tmux session:列不出、看不到缩略图、join 不了、UI 里杀不掉,连 `IDLE_TTL` 都碰不到它 → **永久堆积**。唯一恢复路径是 `manager.ts` 的 Case 3.5(客户端带着 localStorage 里的 id 来 attach),**必须先知道 id**。
- **选型 A vs B(与用户讨论后定 A)**: **A = 目录式**,只读枚举 + `capture-pane` 预览 + 杀,**不收养**。**B = 收养式**,启动时把每个都 `createSession` 进表。选 A 的决定性理由:B 要为 69 个 session 各起一个 tmux client + 2MB RingBuffer(≈138MB),而且 **attach 会按客户端尺寸重设窗口** → 对 64 个跑着 TUI 的 session 同时 SIGWINCH,会把那个 7-agent 的会话强行重排版。**B 其实是 A 的自然结果**:在 A 的列表里点开某个孤儿,走现成的 Case 3.5 就把它变成表里的一等公民 —— 按需付费,而不是启动时一次性付清。
- **实现**: `tmux.ts``listSessions()` + `parseSessionList()`(纯函数,过滤规则可单测)+ `capturePane()`(`capture-pane -p -e`,**不 attach、不 resize**、保留颜色);`manager.ts``listOrphans/captureOrphan/killOrphan/killOrphansIdleSince/countOrphansIdleSince`;`server.ts``GET /orphan-sessions``GET /orphan-sessions/count-idle``GET /orphan-sessions/:id/preview``DELETE /orphan-sessions/:id``DELETE /orphan-sessions?idleDays=N`;前端在首页 grid 下面加一个 **RECOVERABLE** 区,通过 `orphanAsLive()` 适配器**复用现成的卡片/缩略图机制**(不另写一套)。
- **安全护栏(全部在 manager 里,路由无法漏掉)**: ① tmux 名必须是 `web_` + **UUID v4**(复用 `SESSION_ID_RE`,与 attach 协议同一道闸)—— 这既挡住恶意/畸形名字打到 `tmux -t`(字面量 `-t`、路径穿越),也保证**用户自己建的 tmux session 永不被枚举、预览或提供删除**;② 内存表已有的 id 一律拒绝 —— 用 tmux 杀掉会在活着的 PTY 背后终结 shell 并留下僵尸表项,那条路必须走 `killById`;③ `useTmux` 关闭时全部惰性,非 tmux 部署逐字节不变;④ 批量清理**跳过 attached**(`tmux attach` 和第二个 server 进程都算 attached,"本 server 不管它"不等于"没人用")且 `idleDays >= 1`,**没有"删除全部"的形态**;⑤ 读路由无 Origin 守卫(与 `/live-sessions` 同威胁模型),两个 DELETE 都有;⑥ 预览有独立限流桶。
- **对抗式审查发现 10 个缺陷,其中一个是 CRITICAL —— 必须记住的坑**:
- **`#{session_activity}` 是 CLIENT 时钟,不是输出时钟**。它只在 attach 和按键时前进,detached 的 session 无论 shell 打印多少都**永不前进**。tmux 3.6a 实测:detached session 跑 `while true; do echo tick; sleep 1; done`,6 秒后 `session_activity` 仍等于 `session_created`,而 **`#{window_activity}` 精确跟上了输出**。于是"闲置 7 天"实际含义是"7 天没人打字",`Clean up idle 7d+` 会杀掉**正在干活的长跑无人值守会话** —— 正是本 App 存在的理由(vibe coding:丢个任务走开)。**在本机 69 个 session 上实测:旧时钟判死 36 个,新时钟判死 33 个 —— 3 个正在产出输出的会话离被删只差一次点击。**已改用 `window_activity`,并加了一条会在格式串回退时失败的测试。
- **HIGH 无 timeout**:`maxBuffer` 只管洪水不管**等待**,而同步 exec 无法被事件循环打断 → tmux server 被 SIGSTOP 就永久挂死整个 server。5 处调用(含本功能之前就有的 3 处)全部加上。
- **HIGH 同步阻塞轮询路径**:`listSessions` 实测 **73ms**`capturePane` **58ms**,而每台设备首页 5 秒刷一次 —— 每次都是 73ms 事件循环停转,期间**任何设备的任何终端都收不到一个字节**。改为 async(manager 三个函数连带改 async)。服务器首先是 byte-shuttle,轮询路径不许阻塞。
- **HIGH 清理确认框数错了**:数的是**卡片**(封顶 24),而清理匹配的是服务端枚举的全部 —— 本机会显示"24"然后终结 33 个 shell。新增 `count-idle` 路由回答真实数字,拿不到就拒绝执行。
- **MEDIUM ×4**:失败与空被折叠成同一个 `[]`(一次 429 就读成"孤儿全没了",拆掉所有已挂载的 xterm);预览闭锁条件是"卡片建过"而非"预览成功"(丢一次请求就永久空白);refresh 从 3 处进入且中间有 await 却无守卫(慢轮询会复活已杀的卡片、或往已清空的 root 里重挂)—— 加 generation 计数器,每个 await 后校验;`renderPreview` 可能写入已 dispose 的 xterm(xterm 抛 `Object has been disposed`)—— 给 `PreviewCard` 加显式 `disposed` 标记 + `disposeCard()` 统一入口。**刻意不用 `el.isConnected`**:尚未 append 的卡片是活的,混淆二者会丢掉首次渲染 —— 这一点是被两个既有的 preview-grid 测试正确抓出来的。
- **LOW**:kill/cleanup 丢弃返回值,拒绝(代理下 Origin 不符的 403、竞态的 404)看起来像按钮坏了 —— 改为回显。
- **性能/规模决策**: 预览**每张卡只取一次**(不随 5s 刷新重取),网格**封顶 24 张**(每张缩略图是一个 xterm 实例),超出部分**在 UI 里明说**并给出 `tmux attach -t web_<id>` 兜底 —— 不静默截断。
- **验证**: `tsc` 双 project 干净;单测 **2209 全过**(+42);e2e **27/27**;新增孤儿集成测试 **8/8** 跑真 tmux(建一个一次性 session → 列出 → capture 并断言它**仍是 attached=0** → 拒非 UUID id → 拒外来 Origin → 杀掉 → 第二次 404)。**该集成文件刻意从不发起批量 DELETE** —— 它跑在宿主机真实 tmux server 上,会终结开发者自己的会话。`window_activity` 的验证在**独立 socket(`tmux -L`)**上做,全程未触及用户的 69 个 session,事后确认仍是 69。
- **遗留 / 待办**: B(启动收养 + 让 `reapIdle` 管到孤儿)未做,需要配"按需 attach"(`Session.pty` 可空)才划算 —— 那要动 Session 形状和不变量 #2。另:桌面 App 默认 `USE_TMUX=0`(`desktop/src/server-config.ts:41`),所以**在 App 里起的 session 不受 tmux 保活**,退出 App 就会被杀;这与"走开再回来"的初衷冲突,已向用户指出,是否改默认值待定。
- **commit**: `4892fa7`(功能)+ `f6ef19e`(审查修复),合并 `22a7929`
### 🎨 [x] w6 收尾 · 设计稿逐条对照,补掉 Git 面板的落地偏差(2026-07-29/30,worktree `panel-design-fidelity` + `panel-fidelity-leftovers`)
- **起因**: 用户截图说"按钮和文字框还是很奇怪"。**先查客户端再查代码,结论是两件事都成立**: