Overview
dsh-session-delete
README / EN
Package documentation
Registry summary
dsh.pub verifies the pinned bundle contract, runtime facts, and distribution semantics. The complete README remains in the source repository.
Read the full README on GitHubLIMITATIONS
Known limitations
1. **没有“按 id 删除会话”的宿主 API**:`sessions` 只暴露 `get` / `liveEntryFor` / `detachEntered`。内存阶段因此走 `detachEntered`,对**运行态**会话有效;对非运行态(进程重启后)会话该阶段为 `skipped`,行消失依赖第 7 阶段广播 + 第 5 阶段删盘后 `session-query-sqlite` 的自愈对账。 2. **Windows 文件锁**:宿主可能仍持有 `session.v4.jsonl` 句柄,所以磁盘阶段先删、删不掉的记入 `leftover`,等第 6 阶段摘除内存后再重试一次,并把最终残留路径写进报告。 3. **目录定位靠扫描,不靠 cwd 反推**:会话目录里的 slug 是**创建时**的 cwd 快照,之后切换工作目录会与当前 cwd 不一致(本项目会话自己就是这种情况)。扫描限定深度并逐个校验目录名后缀,避免误删。 4. **拒绝删除“未知会话”**:会话来路不明时宁可拒绝(gate 中止),也不去赌一个可能拼错的 id。 5. **客户端不蹭 ui-workspace 的私有 toast/store**:那两样都没导出,插件自建 `shell.overlay` 槽、自建快照 store、自建 CSS。 6. **红色确认按钮**:UI primitives 的 `Button` 只有 `primary/ghost/outline/toolbar`,所以用 `variant: 'outline'` + 自带类名 `.session-delete-confirm`(颜色取 `var(--dsw-alias-state-error-primary)`)。 7. **自建 HTTP 路由而不是 typert**:typert 需要严格 codec 与生成器产物,成本远高于一次同源 `fetch`;插件只在浏览器半边调用自家路由。 8. **不修改宿主快照/索引表**:SQLite 会话索引由宿主自愈对账,插件只删日志与挂账,避免和宿主抢写入。**不删附件**:`dsh-attachment-local` 是内容寻址且跨会话共享的,删了会连累别的会话。 9. **`inject = []`**:三个宿主能力(`sessions` / `workspaceRegistry` / `webServer`)都按需查找,缺失就降级为 `skipped`,插件永远不会拖垮宿主启动。 10. **自带请求准入(审计后加固)**:审计指出宿主 `webServer` 自身不做鉴权,而插件路由能永久删数据,因此加了「来源校验 + JSON 强制 + 一次性令牌」三层(见 §6);令牌只走同源 `fetch`,跨站脚本因无 CORS 头读不到,比较用 `crypto.timingSafeEqual`。客户端同时加了 20 s `AbortController` 超时(取令牌 8 s)与**同步的在飞守卫**(`react.useRef`),宿主无响应或在飞时不能重复提交;令牌轮换(DSH 重启)会触发一次自动重取重试;`deleting` 期间允许关闭模态(不会留下打不开的弹窗)。 11. **账本文件兜底做丢更新检测 + 进程内串行**:服务不可用时直接改 `workspace.json` 有覆盖风险,所以走「stat → 读 → 改 → 再 stat → 一致才 rename」的乐观重试(最多 3 次),仍冲突就如实报 `ledger-contended`,不假装成功。同一进程内的并发删除会把这段 read-modify-write 串成一条 promise 链(第二次审计实测:不串行 + 固定临时文件名会让两个写入互相 `rename` 掉对方的 tmp,把 `workspace.json` 写成非法 JSON),临时文件名也改成“pid + 序号 + 随机后缀”并对失败路径做 `rm` 清理。 12. **失败详情可见**:阶段级 `detail`/`error`/`leftover` 会渲染在模态的「失败详情」区(此前只显示通用 HTTP 错误),409 也会把 `report` 一并带回;**返回值型失败**(`status: 'failed'`)同样写进 `warnings`,避免降级跑完却看起来一切正常。 13. **运行中会话默认仍需 `force`**:UI 在预览里明确写出「该会话正在运行(N 项活动)」,确认按钮变成「强制停止并删除」才带 `force: true`;`allowForce: false` 时该按钮不可用。 14. **磁盘没清干净就不广播**:`api-session/removed` 是客户端隐藏该行的唯一信号,若第 5 阶段失败(Windows 文件占用)仍广播,就会得到「侧栏行没了但日志还在、重启复活」的状态。因此第 7 阶段此时返回 `blocked` 并在模态里列出残留路径;只有第 6 阶段摘除句柄后重试成功,才继续广播。广播本身失败也会如实标 `failed`(`ctx.emit` 不可用时不会假装成功)。 15. **重复挂账一并清除**:`workspace.json` 里同一 id 出现两次时,第 3 阶段会删掉**全部**出现(旧实现只 `splice` 一处,会留下幽灵挂账并在刷新后复活)。 16. **插槽注册的卸载幂等**:`slots.inject` 返回的 disposer 与插件自身 scope 可能都触发卸载,客户端因此用一次性标志 + 逐个 `try/catch` 调用,避免「已释放」异常逃出卸载路径。 17. **客户端测试的边界**:`tests/client-bundle.test.js` 用自建 hook 运行时驱动真实渲染状态,但它仍不是浏览器——没有 DOM、没有真实 `Modal` 焦点管理/Escape、没有真实 `fetch` 与 React 调度,因此「菜单项真的出现在侧栏、弹窗外观、`sessions.refresh()` 真的刷新了列表」只能在真实 DSH 进程 + 浏览器里验证。**已在真实 DSH 里做过一轮(见 §7 末尾)**,但每次改动客户端后仍建议复验一次。 18. **投影缓存必须在内存摘除之后重扫一次**:`session_projcache` 的 domain 记录删掉不等于文件消失——宿主在会话 dispose 时会再写一份最终 projection 快照。真实环境实测:删除成功约 27 s 后,`<storagesRoot>\session_projcache\sessions\<id>.json` 又被写回(内容里还带着会话标题),而 `workspaceRegistry` 的 header 缓存因此继续报 `sessionKnown(id) === true`(`dsh-workspace/lib/index.js:612-617` 的缓存 `headers` 从不按 id 逐条清理)。所以第 5 阶段改成「domain 记录 + 文件」两条路都删,第 6 阶段摘除后再扫一遍,兜住这次写回。 19. **会话目录是目录链接时,必须连链接目标一起删**:用户把会话数据搬到 D:/E: 盘、在原来的位置只留 `mklink /J`(junction)是很自然的做法,而 `fs.rm(dir, { recursive: true })` 遇到 reparse point **只解开链接、不删目标**——实测(Windows `mklink /J`)会留下全部日志文件,旧行为却报告成功:一次「删掉了」的假象。`0.1.2` 因此先 `lstat` 判链接、`realpath` 解析目标,再按目标名分两种策略执行:目标名 == 会话 ID(`encodeSegment(sessionId)`,宿主自己的布局)→ 递归删目标;目标名任意 → **只删目标目录里的会话文件**(`session.jsonl` / `session.v(N).jsonl` + 可选 `.zstd` / `.tmp`、`session.lock`),随后 `rmdir` 目标(只在变空时成功),其余文件原样保留并写进报告 `leftover`;链接指向的目录里**一个会话文件都没有**(= 无关目录或恶意链接)→ 直接拒绝,不递归。链接始终在负载之后才删:负载被占用时链接仍在,第 6 阶段的 `leftover` 重试才有路径可打。 20. **`session.lock` 是真名,别猜成 `<日志>.lock`**:`dsh-session-persistence-jsonl/lib/index.js:643` 的 `LEASE_FILENAME = "session.lock"`(`:667` 拼成 `join(dir, LEASE_FILENAME)`),所以预览里的 `locked` 只认 `session.lock` / `session.v(N).lock` 两种名字;`session.v4.jsonl.lock` 这种臆造名既不会被判锁、也不会被第 5 阶段顺手带走(它不在会话文件白名单里)。同时日志名补齐了第 0 代:`sessionFormatLogFilename(0)` 返回 `session.jsonl`(无 `.vN`),`SESSION_LOG_PATTERN` 已放宽成 `/^session(\.v\d+)?\.jsonl(\.
