C-S-N-Y/dsh-session-cleaner ↗★ 0
dsh-session-maintenance
DeepSeek Harness plugin: permanently delete archived sessions and clean up ghost / orphan session records (host Remote + sidebar UI, zh/en).
インストール
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:C-S-N-Y/dsh-session-cleanerドキュメント
README 全文を読む ↗dsh-session-cleaner
给 DeepSeek Harness (DSH) 用的会话清理插件:永久删除已归档会话、扫描并清理幽灵行 / 孤儿会话。 宿主半(Remote 端点)+ 客户端半(侧栏菜单 / 弹窗 / i18n 中英),零依赖、零构建。
⚠️ 这是破坏性工具:删除是真的删除(
rmSync,不进回收站、不可恢复、不可再被搜索到)。 请先拿npm run make-fixture造出来的假会话试手,别拿真实对话练。 插件强制「只有已归档的会话才能删」——把"归档"当作删除前的二次缓冲。
1. 它解决什么问题
DSH 本身没有"删除会话"这个动作(只有归档)。于是这些会话会一直躺在 ~/.dsh 里:
| 症状 | 本插件做什么 |
|---|---|
| 归档了一堆不想要的会话,磁盘越占越多 | 永久删除:日志目录 + 投影缓存 + 归档标记 + 工作区成员槽,一次清干净 |
| 侧栏留着一行点不动、打不开的"幽灵会话"(文件早没了、归档集合里还挂着 id) | 清理无效会话:扫描后一键摘掉残留标记(可先点「诊断」看宿主眼里的真相) |
| 父会话删了,子智能体(subagent)会话变成永不成行、也无法管理的孤儿,文件一直占着 | 扫描出孤儿会话并显式勾选后删除 |
侧栏「…」菜单里多一项 永久删除会话…,侧栏底部多一项 清理无效会话。
2. 安装
前提:装了 DSH 桌面版;dsh CLI 可用;有 git 和 Node ≥ 20。
# 1) 克隆到你喜欢的位置(示例:DSH 插件目录)
git clone https://github.com/C-S-N-Y/dsh-session-cleaner.git "$env:USERPROFILE\.dsh\plugins\dsh-session-cleaner"
# 2) 以本地链接形式装进你的 profile(profile 名按你自己的来,桌面版通常是 desktop)
dsh plugin --profile desktop add link:"$env:USERPROFILE\.dsh\plugins\dsh-session-cleaner"
然后再确认两处(插件管理器一般会自己写好):
# a) profile 的依赖里出现它,且 node_modules 里是指向源码目录的链接
Get-Content "$env:USERPROFILE\.dsh\profiles\desktop\package.json" -Raw
Get-ChildItem "$env:USERPROFILE\.dsh\profiles\desktop\node_modules" | Select-Object Name,LinkType,Target
# b) profile 的 cordis.patch.yml 里**只有**这一条(不要在这里再写 insert!)
# - id: session-maintenance
# disabled: false
Get-Content "$env:USERPROFILE\.dsh\profiles\desktop\cordis.patch.yml" -Raw
最后重启一次 DSH(宿主插件只在进程启动时加载)。
也可以直接用 GitHub 规格安装(走 pnpm 的 git 路径,插件管理器会先用
git ls-remote探一次仓库):dsh plugin --profile desktop add github:C-S-N-Y/dsh-session-cleaner这条路没有在作者机器上实测过;如果它报错,用上面的clone + link:即可。
卸载:dsh plugin --profile desktop remove dsh-session-maintenance,然后重启。
3. 用法
3.1 删除一条会话
- 先把这条会话归档(侧栏「…」→ 归档);
- 侧栏「…」→ 永久删除会话…;
- 弹窗会把你要删的到底是什么摊开:完整会话 id、工作目录、创建时间、占用体积、是否属于某个工作区;
- 确认 → 删掉。侧栏那一行当场消失,不需要刷新页面、更不需要重启。
如果这条会话还被一个活着的 Agent 持有(DSH 的常态:开过的会话,它的 Agent 就一直挂在宿主内存里), 弹窗会给出 停止并删除 —— 详见 §5。
3.2 清理无效会话
侧栏底部 清理无效会话 → 面板会列出三类(扫描本身是只读的):
| 类别 | 是什么 | 清理动作 |
|---|---|---|
| 幽灵会话 | 归档集合里还挂着、但已经无法正常删除 | 摘标记 + 删残留文件 + 通知页面摘行 |
| 侧栏幽灵行 | 痕迹早删了,却还留在宿主内存里的会话 | 停活 + 摘行(本来就已经打不开、不可恢复) |
| 孤儿会话 | 父会话已删的 subagent 会话 | 需要你显式勾选才会删文件(不可逆) |
面板里还有 诊断 按钮:把宿主眼里的真相(内存会话数 / 归档集合 / 内存有磁盘无)摊开。
4. 安全设计(不是"尽量小心",是写死的不变量)
删除前逐条校验,任何一条不满足就结构化拒绝(不是静默失败):
| 规则 | 拒绝码 | |
|---|---|---|
| I1 | 会话仍被活着的 Agent 持有 | session/attached |
| I2 | 会话上仍有正在运行的工作 | session/running-work |
| I3 | 有活着的子智能体后代 | session/descendants-active |
| I4 | 痕迹不存在 → 幂等返回 deleted:false,而不是报错 | — |
| I5 | 只删已归档会话 | session/not-archived |
磁盘内核(lib/core.js)另有四条防误删不变量,与宿主逻辑解耦、可独立测试:
- S1
sessionId必须匹配^session-[A-Za-z0-9-]{8,}$—— 绝不拼路径后直接删; - S2 目标目录必须落在
/sessions之下(realpath校验,防符号链接逃逸); - S3 目录名必须恰好等于 sessionId,且目录内必须真的存在会话日志文件;
- S4 任何不确定 → 不删,并把原因写进
warnings。
删除顺序也是写死的:先摘归档标记与工作区成员槽 → 再删文件(失败则回滚标记,避免"列表里没有、磁盘还在")。
5. 「停止并删除」为什么必须存在
DSH 里有个反直觉的事实:会话对象的生命周期跟着创建它的 fiber 走。
ctx.sessions只有create / prepare / enter / announce / flush / get / list / fork;ctx.agents只有get / list / roots / register / enter / announce / create / resume;- 两边都没有 remove / detach / dispose(id) —— 插件无权、也无力把已注册的会话/Agent 摘掉。
所以"开过一次的会话,它的 Agent 会一直挂在宿主内存里",而它不代表还在干活。
本插件的处理是(_tryRelease()):
- 停活 ——
workspaceRegistry.archiveSession(id, { stopActivity: true }),这是平台自己的原语 (侧栏"停止并归档"走的是同一条路)。它先落持久归档集合,而归档闸门正是各 provider 的agent/pre-step读的东西 —— 被停掉的工作即使被唤醒也会立刻被挡住; - 等静止 —— 轮询"正在运行的工作 / 活着的后代"直到清空(上限 4 秒,可用环境变量
DSH_SM_RELEASE_TIMEOUT_MS调); - 落盘屏障 ——
ctx.sessions.flush(session):先把缓冲的事件写干净,再删文件, 这样删完之后不会再有"迟到的写入"把日志重建出来; - 之后才放行。有常驻 Agent 时还会在 250ms 后复查一次:真被写回来就再删一次,并在结果里
如实标
resurrected: true。
停不下来(比如工作真的取消不掉)就老实抛 session/stop-timeout 并回传"还剩几项工作",绝不硬删。
6. 已知边界
- 不做附件清理、定时提醒、跨 profile 的 GC —— 只处理"会话本身的痕迹"。
- 侧栏里的 subagent 会话永不单独成行(平台行为),所以这类会话只能通过「清理无效会话」管理。
- 界面里的那一行消失依赖一个宿主转发事件(
api-session/removed,见lib/index.js注释)。 万一通知失败,结果里会带needsRestart:[ids],UI 会如实说"按 F5 刷新页面即可" —— 不需要重启 DSH。 - 本插件不碰
~/.dsh以外的任何文件;DSH_HOME是运行时解析的,测试全靠这一点把删除范围限制在临时目录里。
7. 宿主端点契约
客户端通过 typert Remote 调这四个方法(都在 sessionMaintenance 命名空间下):
| 端点 | 作用 | 破坏性 |
|---|---|---|
probe({sessionId}) | 只读预检:痕迹清单、体积、标题、阻塞原因、能否停活 | 无 |
delete({sessionId, force?}) | 执行删除;force:true = 「停止并删除」 | 不可逆 |
scanJunk() | 扫描幽灵 / 侧栏幽灵行 / 孤儿(纯只读) | 无 |
cleanupJunk({ghosts?, dangling?, orphans?}) | 批量清理;orphans 默认 false(要显式开) | 部分不可逆 |
diagnose() | 宿主侧诊断快照(内存会话、归档集合、内存有磁盘无) | 无 |
8. 开发
cd
npm test # 48 项:40 集成(客户端渲染 + 宿主服务)+ 8 内核;零依赖、不需要 DSH 进程
npm run test:core # 只跑磁盘内核 S1–S4
node test/session-trace.mjs # 只读侦察一条会话的磁盘痕迹
npm run make-fixture -- --cwd C:\tmp\dsh-spike # 造一条可安全删除的假会话
# 读平台**真源码**(DSH 的宿主代码全在 app.asar 里;只读,不写安装目录)
$env:DSH_ASAR = '\resources\app.asar'
npm run platform-src -- "^dsh/node_modules/@deepseek-ai/[^/]+/package\.json$"
npm run platform-src -- "x^" --cat "dsh/node_modules/@deepseek-ai/cordis/src/events.ts" .tmp\events.ts
dsh-session-cleaner/
├─ package.json # dsh.bundle.patch / dsh.client.platform=web / exports["."|"./client"]
├─ cordis.patch.yml # bundle 补丁层:insert 本插件那一行 Loader 行
├─ lib/
│ ├─ index.js # 宿主半(零 import):服务 + typert 端点 + I1–I5
│ ├─ core.js # 磁盘删除内核(S1–S4,可独立测试)
│ └─ client.v17.js # 客户端半:手写 Module Loader 包,无 JSX / 无构建
└─ test/ # 零依赖测试与工具(含只读的 asar 巡检器)
⚠️ 改客户端半必须换文件名(
client.v17.js→client.v18.js)并同步package.json的exports["./client"]与文件里的BUILD指纹:/plugins的响应是cache-control: immutable,而 revision 只看mtime/ctime/size—— 不改名时浏览器会用旧代码, 刷新和重启都救不回来。改名后必须重启一次 DSH(宿主把 bundle 路径缓存在内存里)。 文件名里的v17就是这么来的:开发期每次改内容都换一次名,不是版本号。
9. 常见问题
Q:点了删除没反应 / 提示"仍被一个活着的 Agent 持有"? A:用 停止并删除。DSH 不会回收空闲 Agent,这是常态,不是错误。
Q:删完了那一行还在,只有重启才消失?
A:那是修复前的行为。现在删除成功后会立刻发 api-session/removed 通知页面摘行;万一通知失败,
按一次 F5 即可(宿主那份列表是以磁盘为准的,文件删了它就查不到了)。
Q:菜单项不出现?
A:按顺序查:① profile 的 node_modules 里有没有它;② profile 的 cordis.patch.yml 里那条
disabled: false 在不在;③ 重启过 DSH 没有。客户端半崩溃会被插槽的 error boundary
静默退役(菜单项直接消失),所以也要看 DSH 的日志里有没有 slot entry crashed。
Q:能恢复吗?
A:不能。删除走的是 rmSync,不进回收站、也无法再被搜索到。这也是「只能删已归档会话」这条
硬约束存在的原因。
10. 贡献
Issue / PR 都欢迎。约定:
- 中文注释为正文语言;每条结论请附可复查的证据位置(文件 + 行号),不要写"我记得";
- 改删除相关行为必须同时在
test/里加用例,并且只用假会话(npm run make-fixture); - 破坏性操作一律"先说明、再执行",不接受静默失败。
License
MIT © 2026 C-S-N-Y
English summary
dsh-session-cleaner is a plugin for DeepSeek Harness (DSH) that permanently deletes archived sessions and cleans up leftover "ghost" / orphan session records.
- Permanently delete a session: host log dir + projection cache + archive marker + workspace slot. Only archived sessions can be deleted (archive = the confirmation buffer).
- Clean up junk: scan (read-only) for ghost archive markers, rows whose files are already gone, and orphaned subagent sessions; then clean them with explicit opt-in for destructive parts.
- Stop and delete: DSH never reclaims an idle Agent and exposes no remove/detach API, so the
plugin stops activity with the platform's own primitive (
archiveSession({stopActivity:true})), waits for the session to go quiet, flushes its log, and only then deletes. - Safety: five invariants (I1–I5) plus a disk kernel with four anti-mistake guarantees (S1–S4,
path-escape and directory-name checks). Deletion is irreversible — test with
npm run make-fixture. - Zero dependencies, no build step; 48 dependency-free tests (
npm test).
Install with DSH's CLI:
git clone https://github.com/C-S-N-Y/dsh-session-cleaner.git
dsh plugin --profile desktop add link:
# then restart DSH