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).

包名
dsh-session-maintenance
兼容性
待验证
版本
0.2.0
许可证
MIT
最近更新
2026年10月1日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:C-S-N-Y/dsh-session-cleaner

dsh-session-cleaner

给 DeepSeek Harness (DSH) 用的会话清理插件:永久删除已归档会话、扫描并清理幽灵行 / 孤儿会话。 宿主半(Remote 端点)+ 客户端半(侧栏菜单 / 弹窗 / i18n 中英),零依赖、零构建。

license

tests

deps

topic

⚠️ 这是破坏性工具:删除是真的删除(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 删除一条会话

  1. 先把这条会话归档(侧栏「…」→ 归档);
  2. 侧栏「…」→ 永久删除会话…;
  3. 弹窗会把你要删的到底是什么摊开:完整会话 id、工作目录、创建时间、占用体积、是否属于某个工作区;
  4. 确认 → 删掉。侧栏那一行当场消失,不需要刷新页面、更不需要重启。

如果这条会话还被一个活着的 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()):

  1. 停活 —— workspaceRegistry.archiveSession(id, { stopActivity: true }),这是平台自己的原语 (侧栏"停止并归档"走的是同一条路)。它先落持久归档集合,而归档闸门正是各 provider 的 agent/pre-step 读的东西 —— 被停掉的工作即使被唤醒也会立刻被挡住;
  2. 等静止 —— 轮询"正在运行的工作 / 活着的后代"直到清空(上限 4 秒,可用环境变量 DSH_SM_RELEASE_TIMEOUT_MS 调);
  3. 落盘屏障 —— ctx.sessions.flush(session):先把缓冲的事件写干净,再删文件, 这样删完之后不会再有"迟到的写入"把日志重建出来;
  4. 之后才放行。有常驻 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