Skip to content

better-dsh-session-deletetool

Verified

better-dsh-session-deletetool · v0.4.1 · MIT · Web UI

Delete a conversation from the DSH sidebar instead of only archiving it, and see the whole family it spawned — every subagent session and derived conversation, nested — before choosing which of them go with it; delete many at once from the trash icon besi

Install

dsh plugin add better-dsh-session-deletetool

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Creators

Readme

better-dsh-session-deletetool

真正删除 DeepSeek Harness 里的会话 —— DSH 自带的能力只有归档。

English


为什么需要它

DSH 只能归档会话:归档把那一行从侧边栏隐藏,工件一样不动,日志、工程记录、投影缓存都留在磁盘上,随时可以恢复。持久化接口本身根本没有删除能力,@deepseek-ai/dsh-session-persistence-jsonl 写得很直白:

Nothing deletes session files —— 日志在 root 下不断堆积,直到被外部删除;这一层没有删除接口。

于是用得越久,会话只增不减。这个插件补上缺失的那一步:在会话的 ⋯ 菜单里加一项删除会话,真的把数据抹掉;它弹出的确认框也是唯一能看清一个会话全家结构的地方。

删了什么

对选中的会话,宿主半会:

  1. 删掉会话工件目录 —— 当前日志、保留的每一代历史格式、以及目录里的会话本地文件(路径由 ctx.sessionPersistence.locate(header) 给出);
  2. 删掉你勾选的子会话 —— 子会话是各自独立的 session,各有各的日志。DSH 把每个子会话的父 id 记在它自己的 header 里(SessionHeader.parentSession),而且它创建的每种关系都写这个字段:子智能体由运行时写入并带 origin: 'subagent',fork 出来的对话写入并带 isSeeded,Agent Teams 的名册也靠同一个字段解析。插件按这条血缘广度优先遍历,再与持久化的 subagentCatalog 投影(ctx.subagents.listDescendants)取并集,因此连 header 已经读不出来的子会话也能被点名。删除从最深的往下,集合在动手之前先收集完,一次最多删 200 个,而且只删勾上的那些;
  3. 摘掉工程记录 —— 每个 id 从每个 Workspace 记录的 sessionIds 里移除(Workspace.detachSession),并从注册表全局的归档集合、置顶集合里移除;
  4. 删掉投影缓存 —— session_projcache 域里的记录和磁盘上对应的 <id>.json;
  5. 关掉它拥有的终端 —— ctx.terminals 不属于任何一条准入族,服务要等 Agent 被释放才回收,而那时日志早就没了;
  6. 通知页面 —— 每个被删的 id 各发一次 api-session/removed,这是官方会话控制器销毁会话时发的同一个事件,侧边栏的行会立刻消失。

看清家族,再选择删什么

侧边栏里,子智能体和派生对话都是各自独立的一行,看不出是谁生的,DSH 本身也没有任何地方画这条关系。这个确认框是唯一画出来的地方:整个家族画成一棵树,包括派生对话自己带出来的下一层。

  • 第 1 行是本对话,它的勾选框管整个家族,名字跟侧边栏显示的一致;
  • 第 2 行是一个可折叠的「子智能体(n)」分支,装本对话自己生的子智能体;
  • 第 3 行是一个可折叠的「派生对话(n)」分支,分支里每个派生对话自带它自己的「子智能体(n)」子分支,所以派生对话的派生对话画在父辈块里面(缩进多一格),而不是跟它并排;
  • 子智能体自己派生出来的对话,同理画在那个子智能体的块里面;
  • 每行都有勾选框,标出名字和 id 尾号,还开着的或还有工作没跑完的会带状态标记;名字跟侧边栏同一套规则:日志里有名字用名字,没有就用它项目目录的末段,再没有才用 id,所以不会显示成"未命名";
  • 层级只靠缩进表达:一级缩进一格、二级两格,父行后面紧跟它自己画出来的子行,所以第 3 层那行一眼能看出是挂在上面第 2 层那行下面,而不是"反正更深一点"。

每行的勾选框管它自己那棵子树:下面全都勾上时它是勾上的,只勾了一部分时显示成半选,按一下整棵子树一起勾或一起取消;分组标题的勾选框管这个分组列出的所有行。删除集合只存这一份 id,不再有"选中的根 + 排除的子行"两套状态,所以底部的计数不会和画面对不上。

读不到的血缘会写在列表上方(比如某个分支的目录打不开),而不是静默少几行——少行和"这个会话确实没有子会话"原来长得一样。

确认时只提交勾上的 id,按钮上写着即将删掉几个会话。没勾的子会话会作为独立的会话根留下来。选择会在宿主侧拿它自己走出来的血缘做校验,请求只能点到这个家族里的会话。fork 出来的对话本身也是独立会话,所以它是"带勾选框列出来",而不是被静默带走。

批量删除

工作区标题行、搜索图标左边多了一个垃圾桶图标(画的是 deleting icon.svg 那个图),点开是一个总览所有会话的弹窗:

  • 按工作区分区。 分区顺序跟注册表里工作区的顺序一致,标题是工作区名,右边写这个区里有几个会话;没有被任何工作区认领的会话归到最后一个未归类区。
  • 每个会话画成和单会话确认框同一棵树:会话行下面是它自己的「子智能体(n)」和「派生对话(n)」两个可折叠分组,派生对话再把自己那一层接在它自己的块里面,层级只靠缩进表达。
  • 会话名跟侧边栏一致:日志里有名字就用名字,没有就用它项目目录的末段(比如 Project_lzc),再没有才用 id。没改过名的会话因此显示成目录名,而不是"未命名"。
  • 勾选按子树走,子会话可以单独删。 勾上母会话,它下面勾中的子会话和派生对话跟着一起删;只勾某个子智能体、不勾它的母会话时,那个子智能体自己作为一个删除根提交,母会话保持不动。底部按钮始终写着这次会删掉几个会话。

弹窗里同样有「停止并删除」那个开关,默认打开,和单会话确认框一致。确认后每个被勾的会话根各自走一次上面那条删除流程;某个根失败只在弹窗里列出来,其余照删。

什么会拦,什么不会

只有"还有工作在跑"会拦,"还开着"不会。 门槛用的是 DSH 自己归档时的那条 workspace/session-activity 瀑布:Agent 注册表报进行中的回合,后台任务注册表报后台任务,子智能体运行时报运行中的子会话,定时提醒报生效中的任务。这个询问是对删除集合里的每一个会话各问一次,因为这条准入是按被问的会话作答的:只问目标的话,子会话正在跑的回合或后台任务根本不会出现。确认框会把还在跑的东西写在对应行上、也写在汇总里,并提供停止并删除——先对每个忙碌的会话派发一次 workspace/session-stop,和 archiveSession(id, { stopActivity: true }) 完全一致。

宿主内存里还持有这个会话(ctx.sessions / ctx.agents)时照样删。本机实测(Windows):文件句柄还开着时 rm 依然成功,目录项立刻消失,之后的写入落进已删除的文件而不是把它复活。早先的版本会拒绝这种会话并提示"先切换到别的会话",结果把没显示在窗口里的会话也一起误伤了,那条规则已经去掉。

已知边界

  • 附件二进制不删。 上传的图片和文件存在 ~/.dsh/attachments/v1/objects/<hash>,是内容寻址的去重仓库、多个会话共享,服务本身没有引用计数,所以这些 blob 会留下。
  • 搜索索引会自己收拾。 dsh-session-query-sqlite 是派生索引,每次检索前都会跟持久化对账,被删掉的源从下一次检索起就查不到了。
  • 会话仍被占用时可能留下一条死缓存。 删除时宿主还持有它的话,它被销毁的那一刻投影缓存可能再写回一条 checkpoint。那条记录永远读不到(没有日志就没有会话),只是几十 KB 的死文件。
  • 改插件代码要重启 DSH。 如果 profile 的 HMR 不监视模块文件,替换已安装包里的代码需要重启;客户端那一半刷新页面即可。

安装

下面几条都走 DSH 自带的插件管理,它接受一个安装 spec(@deepseek-ai/dsh-plugin-manager):registry 包名、git 主机简写、仓库 URL、tarball,或者本地绝对路径。npm 那条也可以直接用 npm install 装进 profile 目录。按你的网络情况挑一条。

1. 从 npm 装(包名 better-dsh-session-deletetool)

走 registry,也是唯一不需要访问 github.com 的:

npm install better-dsh-session-deletetool

DSH 自带的插件管理直接接受 registry 包名,在应用里走这条更省事,它会往 profile 记一条依赖并重载。

  • 在应用里:设置 → 插件 → 安装入口,粘贴 better-dsh-session-deletetool。
  • 在会话里:plugin_manager { action: "install_bundle", target: "better-dsh-session-deletetool" }。

要钉版本就照 npm 的写法:[email protected]。

2. 直接从 GitHub 装(需要能访问 github.com)

spec:

github:Lzcdebear/better-dsh-session-deletetool

等价写法:

https://github.com/Lzcdebear/better-dsh-session-deletetool

把 spec 交给 DSH:

  • 在应用里:设置 → 插件 → 安装入口,粘贴 spec。(插件管理界面和 plugin_manager 工具接受的是同一个字符串。)
  • 在会话里:让 Agent 装 —— 工具调用是 plugin_manager,action: "install_bundle",target: "github:Lzcdebear/better-dsh-session-deletetool"。

可以用 # 钉住某个 ref:github:Lzcdebear/better-dsh-session-deletetool#v0.1.0。

连接检查失败时 DSH 会给出一个有界的日志路径。如果这台机器根本连不上 github.com,走第 1、3 或第 4 条,或者先给 git 配上代理(git config --global http.proxy http://127.0.0.1:7890)。

3. 从本地副本装(离线可用)

把仓库下载下来(ZIP 或 git clone),解压到任意位置,然后把绝对目录交给插件管理:

plugin_manager { action: "install_bundle", target: "D:\\plugins\\better-dsh-session-deletetool" }

DSH 会记一条 link: 依赖并重载 profile。开发这个插件用的就是这条路,网络受限时也走它。

4. 从发布 tarball 装

https://github.com/Lzcdebear/better-dsh-session-deletetool/archive/refs/heads/main.tar.gz

安装入口同第 2 条;git 不可用但 HTTPS 通的时候好用。

装完之后

宿主半随 profile 一起加载;客户端半刷新页面后出现。会话的 ⋯ 菜单里会多一项删除会话 / Delete conversation。

想卸载就用同一个管理入口:plugin_manager { action: "remove_bundle", target: "better-dsh-session-deletetool" }(bundle 键就是包名)。

用法

  1. 在任意会话行上点 ⋯,选删除会话。
  2. 确认框先向宿主问一次真实状态,写清楚:会删掉什么、它是不是还被 Harness 占用、还剩什么在工作。
  3. 接着把整个家族画成一棵树让你挑:本对话一行,下面按「子智能体(n)」「派生对话(n)」两个可折叠分组展开,派生对话把自己那一层接在自己的块里面;每行都有勾选框(带标题、id 尾号和状态标记),分组标题的勾选框管这一块。不想删的把勾去掉。
  4. 确认。按钮上写着即将删掉几个会话;如果还有在跑的工作,按钮会变成停止并删除,按下去先停掉那些工作。
  5. 相关的行立刻从侧边栏消失,数据也从磁盘上没了。

批量的走法:点工作区标题行里、搜索图标左边那个垃圾桶图标,在按工作区分好区的列表里勾会话(勾母会话连带子会话,子会话可以单独勾),然后确认。

HTTP 接口

客户端半通过 ctx.webServer 上四条同源 exact 路由访问宿主。纯 JS、无构建步骤的插件没法声明带类型的 ctx.remote 命名空间(那需要 Typert 生成产物),所以这里用的是社区插件 dshmarket 同款通道。

路由 方法 作用
/better-dsh-session-deletetool/inspect?sessionId=… GET 返回 stored / open / agent / running / activity / artifactDirectory / warnings(没读到的血缘线索,避免"少几行"被当成"这个会话没有子会话"),以及 descendants = { count, subagents, derived, truncated, maxDeletable, items[] },其中每项带 id、kind、depth、parentId、title、open、agent、running 和它自己的 activity
/better-dsh-session-deletetool/delete POST body { sessionId, stop?, descendants? }——descendants 不传表示整个家族,传空数组表示只删它自己;返回 removed、descendants(每条带 kind)、kept、stoppedActivity、terminalsKilled、warnings、runtime、activity。点到的名字不在家族里会在动手之前就以 400 unknown-descendant 拒绝
/better-dsh-session-deletetool/catalog GET 返回 { ok, workspaces: [{ key, workspaceId, title, path, sessions[] }], totals };每个会话行带 id、kind(root / subagent / derived)、depth、parentId、hasChildren、family、subagents、derived、title、cwd、open、agent、running、activity。分区顺序就是注册表里工作区的顺序,workspaceId 为 null 的那个分区是"未归类"
/better-dsh-session-deletetool/delete-batch POST body { roots: [{ sessionId, descendants? }], stop? },每个根各自照单会话那条规则走一遍;返回 { ok, roots, removed[], failed[] }。某个根失败只记一条 failed,不会中止其余。一次最多 200 个根

四条路由各自带同源校验:Host 必须是环回、sec-fetch-site 不能是 cross-site、Origin 存在时必须与 Host 同源。别的网站页面打不进来。

构成

文件 作用
host.js 宿主半:四条路由、工件/工程记录/缓存清理、子会话子树、按工作区分区的会话总览
client.js 客户端半:sidebar.workspaces.session.menu.item 菜单项(order 500)+ 批量图标 + 两个 shell.overlay 对话框
cordis.patch.yml 把宿主行插进 profile 层栈
test/host.test.mjs 路由测试,跑在真实临时目录上
icon.svg 插件图标(批量按钮画的是 deleting icon.svg 那个图)

样式只用宿主主题 token(--dsw-alias-*),浅色深色都跟随;界面文案通过客户端 locale 服务本地化(英文 / 简体中文)。

批量入口为什么不注册一个新插槽:sidebar.workspaces 是 single 槽,第二个注册者会顶掉官方那套会话浏览器,而不是并排。所以客户端半注册在 sidebar.footer.action(本身不渲染任何东西),再把自己的按钮用 portal 挂进浏览器自己的搜索格子里,并用一个 MutationObserver 在 React 重渲染表头时补回来。这一步除了"打开弹窗"没有任何权限,真正干活的是宿主那两条路由。哪天 harness 改掉了这个格子的 CSS 类名,按钮会不出现,别的地方不受影响。

开发

node --test test/host.test.mjs

测试直接驱动真实的 apply() 注册,配假的宿主服务和真实的临时目录,覆盖:删除本体、有活时的拦截与停止、子会话子树、状态查询,以及各种拒绝路径(坏 id、坏 body、错方法、跨站来源)。

许可

MIT

作者