All plugins

DSH / BUNDLE / CLIENT-UI

dsh-session-delete

v0.2.2Baboo-ming / dsh-session-delete57ccebd8d0

InstallableBundlesUI & client pluginsCommunity · Topic auto-analysisWeb UI

Overview

dsh-session-delete

Permanently delete a DSH session from the sidebar session row menu (danger-styled), or delete every session of a workspace from the workspace row '...' menu, with a confirm modal and a seven-stage, independently-degrading deletion pipeline (validate → running guard → workspace ledger → archive set → on-disk logs → in-memory drop → broadcast).

README / EN

Package documentation

Registry summary

Permanently delete a DSH session from the sidebar session row menu (danger-styled), or delete every session of a workspace from the workspace row '...' menu, with a confirm modal and a seven-stage, independently-degrading deletion pipeline (validate → running guard → workspace ledger → archive set → on-disk logs → in-memory drop → broadcast).

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 GitHub

LIMITATIONS

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(\.