Memory
dsh-session-delete-bigpizza
BigPizzaV3/dsh-session-delete · ★ 1
Permanently delete stored dsh Sessions (one Session or a whole Workspace group) from the sidebar.
README body is shown in Chinese.
Manifest details
安装
dsh plugin --profile web add 'github:BigPizzaV3/dsh-session-delete#d01d3be03482f957e4f1723742c5835b19e04e53'GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
给 dsh 补上官方刻意没有的「删除会话」。dsh 0.2.x 里会话只能归档:
dsh-session-persistence 与 dsh-session-persistence-jsonl 的文档都写明
「seam 无删除接口,日志在 root 下累积,直到外部移除」,
dsh-client-ui-workspace 也把「没有 Session 删除」列为已知限制。
本插件在不修改 app 本体的前提下,把「外部移除」这一步做成可点的操作。
它做什么
删除一个会话,或一个 Workspace 分组下的全部会话。每删一个会话,清理两处磁盘产物:
| 内容 | 路径 |
|---|---|
| 会话日志目录 | $DSH_HOME/sessions/<项目目录>/<session-id>/(读 ctx.sessionPersistence.root,缺省 ~/.dsh/sessions) |
| 投影缓存 | $DSH_HOME/storages/session_projcache/sessions/<session-id>.json |
| 粒度 | 入口 | Host 路由 |
|---|---|---|
| 单个会话 | 会话行 … → 删除会话…(slot sidebar.workspaces.session.menu.item,order 500) |
POST /api/dsh-session-delete/session |
| 整个分组 | 分组行 hover → 垃圾桶按钮;或任意会话 … → 删除分组内全部会话…(order 600) |
POST /api/dsh-session-delete/workspace |
| 单个会话(无 UI) | /session-delete <session-id> |
同一 Host 实现 |
分组删除默认包含已归档会话,确认框里可以用 同时删除已归档会话 取消勾选;「未分组」桶也可以整组删除。
为什么走 Fetch 路由,而不是 RPC 通道
宿主半侧用 ctx.connection.fetch.register({ path, methods, requestBody, fetch })
注册一条 /api/... 下的精确 Fetch 路由——和随附的 /api/session.export
(会话日志导出)、/api/session/uploadFileBinary(附件上传)完全同一套机制,
由 Connection 已经挂好的 /api 前缀路由做 Host/Origin 校验 + 浏览器认证,因此
插件只需要 inject: ['connection', 'sessions']。
不要改成 ctx.connection.rpc.handle()。 那个方法在
dsh-client-connection/lib/index.js:656 用 owner.webServer.register(route) 注册路由,
而 owner 来自服务自身的 shadow context;dsh-client-connection 只在自己的
ctx.inject(["webServer"], …) 子上下文里用过 webServer,根上下文从不注入它,
所以任何调用方都会撞上 cannot get property "webServer" without inject
(0.2.0-rc.2 里没有任何随附包调用 rpc.handle,只有 intercept 和 fetch.register 的调用方)。
1.0.0/1.0.1 就是踩了这个坑。
安全边界
- 先关闭,再 unlink:目标还挂在
ctx.sessions/ctx.agents里时,宿主先用 agent 工厂自己的 teardown 句柄(agentLoop.prepare()返回的dispose,和 ACPsession/close同一条路径) 取消回合、等待 idle、关闭 JSONL 写句柄并释放写锁、从两个 store detach;只有这些都完成后才rm目录。因此不会出现「写入方继续往已解除链接的 inode 追加」的静默丢数据。 - 拿不到句柄才排队:极少数在插件加载前就已发布、且控制器 effect 也定位不到的活会话,仍写进
session-delete-pending.json,下次启动时删除;这是兜底,不再是「打开过会话」的常态。 - 拒绝路径穿越:
sessionId必须是单一安全路径段,含..或/一律 400。 - 分组成员由 Host 解析:浏览器只传
workspaceId(或null表示未分组),Host 用workspaceRegistry.get(id).sessionIds取成员;未分组由 Host 列会话并排除origin: 'subagent'的子代理。客户端递来的 id 不会越过分组边界。 - 分组 Header 是装饰,不是替换:随附 Workspace Header 的
…菜单是硬编码的rename/delete数组,没有 additive slot。浏览器半侧只在已渲染的[data-row-key^="workspace:"]行里插入一个 hover 垃圾桶按钮,并用 capture 阶段代理点击; 找不到rowActions结构时自动跳过,不替换/重绘官方 UI。会话菜单里的同一分组功能始终可用。 - 找不到就拒绝:不会猜测 cwd 编码,而是在会话根下逐个项目目录找同名目录(涵盖
_no-cwd)。 - 不碰附件:
$DSH_HOME/attachments/v1是内容寻址的共享对象,按会话删会连累其他会话,本插件不动它。 - 不可撤销,也没有回收站。
安装
先在任意位置拿到本仓库(git clone https://github.com/BigPizzaV3/dsh-session-delete.git,
或直接下载 zip 解压)。安装入口都要求这个目录的绝对路径,并会在 profile 目录跑 pnpm、
把包选进 dsh.profile.bundles——不要手工改 profile 的 package.json / cordis.patch.yml
(随附的 cordis-plugin-development 技能明确禁止):
- A. Web 侧栏「插件」页 → 添加插件 → 填本仓库目录的绝对路径,例如
~/Documents/dsh-session-delete(展开成绝对路径)。 - B. 切到
cordis(Creator)会话预设,让 agent 用plugin_manager工具install_bundle,target同样是那个绝对路径。
也可以先跑一遍自带测试确认宿主侧逻辑(纯 Node,无依赖):
node test-host.mjs # 64/64
node test-client.mjs # 52/52
本机 bundle 已经装入(profile 的 node_modules/@local/dsh-session-delete 是指向本目录的
符号链接),因此装/改代码后重启一次进程即可加载新的模块代际,不必重装。cordis-plugin-loader
用裸 import() 且无缓存击穿,hmr.root: [] 不热重载模块——改 JS 必须重启才加载新代码;
只有改 cordis.patch.yml 配置才走 HMR 即时生效。注意:这次重启只是为了加载补丁,删除操作本身
不再需要重启。卸载:插件页卸载该 bundle,或 plugin_manager remove_bundle。
使用
- 删单个会话:会话行
…菜单点 删除会话…,或在输入框执行/session-delete session-xxxxxxxx-...。 - 删一个分组:把鼠标移到分组行上,点垃圾桶按钮;或在组内任意会话的
…菜单点 删除分组内全部会话…。确认框会显示分组成员数,默认勾选 同时删除已归档会话; 取消勾选就只删当前可见的(未归档)成员。点确认后 Host 逐个关闭/删除并返回摘要 (已删除、已排队、原本不存在、失败数)。 - 冷会话(本次启动以来没被打开过):立即删除日志目录 + 投影缓存,宿主半侧同时
把该 id 从 Workspace 记账里
detachSession掉;浏览器半侧ctx.sessions.refresh()重拉会话基线,侧栏那行当场消失;若删的正是当前主视图,先uiWorkspace.clearMain()释放引用,不会停在「历史加载失败」上。 - 已打开的活会话:宿主先发
workspace/session-stop停掉该会话的回合、子代理、任务和终端, 再调用工厂的 teardown 句柄(取消 → 等 idle → 关闭写句柄/释放 lease → detach)。session/disposed会转成 Session 控制器的api-session/removed,所有已连接的侧栏当场移除该行,随后才删 日志目录与投影缓存。整个过程不需要重启。分组删除会逐个会话走同一条路径。 - 兜底队列:只有「活会话存在、但工厂句柄和控制器 effect 都拿不到」的极端情况才写
$DSH_HOME/storages/session-delete-pending.json,对话框显示「已安排删除,重启后消失」; 插件在下次启动时(apply阶段,浏览器还没挂载任何会话)删除并清空队列。分组摘要里这部分 计入「已排队」。
为什么现在能立即删 live 会话
会话被打开后 attach 到宿主进程,ctx.sessions / ctx.agents 的 enter() 只把 detach 句柄交给
创建它的工厂(dsh-agent-loop),没有任何公开 API 能让第三方插件卸载一个活会话。但工厂在
prepare() 里已经返回了 { agent, dispose }——ACP 的 session/close 用的正是这个 dispose。
本插件在激活时包住 agentLoop.prepare(),按 Session id 记住这个 teardown 句柄;删除时直接调用
它,因此能先安全关闭写入方再 unlink。若某个会话在插件加载前就已发布,则回退到控制器 fiber 上的
agentLoop.lifecycle(<id>) owner effect;两条路都不可达时才排队。
1.0.2 及更早:文件删掉了,但侧栏行要手动刷新页面(
Cmd+R)才消失——那是浏览器缓存的 会话基线快照,删除动作本身不产生任何 UI 事件。1.0.3 起冷会话自动刷新;1.0.7 起活会话走session/disposed→api-session/removed,连刷新都不需要。
已验证 / 未验证
- 宿主半侧:
node test-host.mjs(64/64 通过)覆盖两条路由注册形状(/api前缀、 POST、buffered)、冷会话立即删除、投影缓存清理、Workspace 记账 detach、活会话先workspace/session-stop→ 调用工厂dispose→ 确认 dispose 时文件还在 → 之后才删目录、 拿不到句柄时的排队兜底 → 下次启动扫描删除 → 队列清空、分组删除的 archived 过滤、 Workspace 记账同步、缺失分组 404、非法 body 400、未分组桶、subagent 排除、..穿越拒绝、 未知 id 拒绝、非 JSON 请求体、_no-cwd项目、命令入口;并用 Cordis 风格的 inject 守卫断言 宿主半侧 不再触碰webServer。 - 客户端半侧:
node test-client.mjs(52/52 通过)用最小 React 替身(带 hook 槽)加载window.__ModuleLoader__模块,断言 slot 注册形状、中英两套文案(单会话/分组菜单项、 对话框标题/正文/按钮、排队提示、活会话拒绝、分组摘要/失败)、菜单行悬停高亮与键盘焦点环、 点击行打开对话框、分组菜单在单成员组隐藏、archived 勾选改变计数、分组请求成功摘要与 「找不到分组」本地化、未分组桶、成功删除关闭浮层并刷新基线、蒙板 token,以及源码里 没有window.confirm/alert调用、分组 Header 装饰选择器存在。 - 仍未做:在真实页面上点一次分组垃圾桶(DOM 装饰的视觉与 React reconciliation 行为);
分组功能在会话
…菜单里有同一实现的备用入口,不依赖该装饰。
变更记录
- 1.0.8 — 新增删除整个分组:宿主新增
POST /api/dsh-session-delete/workspace({ workspaceId, includeArchived }),由workspaceRegistry解析分组成员并逐个复用单会话的 stop → dispose → detach → rm 路径,返回deleted/queued/missing/failed摘要;workspaceId:null走未分组桶并排除 subagent。客户端新增分组确认框(成员数、已归档勾选、摘要/失败列表), 并在分组行 hover 区插入一个垃圾桶按钮;由于随附 Header 菜单是硬编码数组、没有 additive slot, 该按钮是 DOM 装饰,找不到结构时自动跳过。同一功能也挂在任意会话…菜单(order 600)作为 不依赖 DOM 装饰的备用入口。测试:宿主 64/64、客户端 52/52。 - 1.0.7 — 修复「删除会话必须重启才生效」。宿主在激活时包住
agentLoop.prepare(),按 Session id 捕获每个 live Agent 的工厂 teardown 句柄(与 ACPsession/close同一个dispose);删除时先workspace/session-stop,再dispose()(取消回合 → 等 idle → 关闭写句柄/释放 lease → 从ctx.agents/ctx.sessionsdetach → 触发session/disposed),随后才rm日志目录并清理投影 缓存。session/disposed由 Session 控制器转成api-session/removed,侧栏行立即消失,不再需要 重启。投影缓存删除会等待 storage domain 写链并重试,避免session/disposed的异步 checkpoint 把文件写回来。只有句柄与控制器 effect 都不可达的活会话才保留排队兜底。测试:宿主 44/44、客户端 34/34。 - 1.0.6 — 支持删除已挂载会话:宿主侧在
$DSH_HOME/storages/session-delete-pending.json维护待删队列——目标 live 时不 unlink(会静默丢数据且留下幽灵行),而是排队,并在插件apply(下次启动、浏览器尚未挂载任何会话)时扫描删除、清空队列;客户端对话框新增 「已安排删除 / 知道了」状态,并把session-delete/not-found、session-delete/bad-request映射成中文文案,不再回显宿主英文原文。测试:宿主 28/28、客户端 29/29。 - 1.0.5 — 两个观感问题的修复:
- 菜单行没有选中效果:自绘行补上悬停/焦点态——背景用
--dsw-alias-interactive-bg-hover-danger(回退--dsw-alias-interactive-bg-hover), 键盘焦点加--dsw-alias-border-l4焦点环,并加了一枚自绘 14px 描边垃圾桶图标, 与相邻的 pin/rename/fork/archive 行对齐。 - 蒙板盖住整个界面:之前误用
--dsw-alias-bg-overlay——那是实色表面色 (做计数徽标背景用的),所以铺满全屏就成了不透明挡板。改用真正的弹窗蒙板--dsw-alias-bg-mask-1(亮色#0000003d/ 暗色#00000080)加--dsw-mask-blur,并用随附弹窗同款写法top: var(--dsh-frame-chrome-top, 0px)让蒙板从窗口顶栏下方开始,不再压住标题栏。
- 菜单行没有选中效果:自绘行补上悬停/焦点态——背景用
- 1.0.4 — 文案接入 client locale:
ctx.locale.register('sessionDelete', { zh, en }), slot 注册带locale: NS,因此跟随 Harness 语言(之前用navigator.language,在中文 UI 里判成了 en-US 而显示英文);确认框改为自绘浮层(shell.overlay+useSyncExternalStore的待确认状态),不再使用window.confirm/alert,样式用--dsw-alias-*主题变量。 新增test-client.mjs。 - 1.0.3 — 修复「删除了但侧栏行不消失」:浏览器半侧删除成功后调用
ctx.sessions.refresh()(重拉session.list,缺失的 summary 会被mergeOrderedBaseline丢弃),被删会话若正是当前主视图则先uiWorkspace.clearMain();宿主半侧新增workspaceRegistry的detachSession清理。测试增至 24/24(含 detach 断言)。 - 1.0.2 — 把宿主侧通道从
ctx.connection.rpc.handle()换成ctx.connection.fetch.register()。前者在 0.2.0-rc.2 里对任何调用方都会抛cannot get property "webServer" without inject(见上「为什么走 Fetch 路由」); 后者是随附导出/上传路由同款机制,inject回到['connection', 'sessions']。 浏览器半侧改为对/api/dsh-session-delete/session发 POST JSON。 - 1.0.1 — 尝试给
rpc.handle补webServer注入(无效,原因见上)。 - 1.0.0 — 首个版本。
文件
| 文件 | 作用 |
|---|---|
package.json |
bundle 清单:dsh.bundle.patch + dsh.client(platform: web) |
cordis.patch.yml |
插入一行宿主插件 session-delete |
index.js |
宿主半侧:/api/dsh-session-delete/session Fetch 路由 + /session-delete 命令 + 活会话 teardown + 删除实现 |
client.js |
浏览器半侧:单会话/分组菜单项、分组确认框、分组 Header hover 装饰、相对路径 fetch 调宿主 |
locale/{en,zh}.json |
插件页展示元信息 |
test-host.mjs |
宿主半侧离线冒烟测试(含活会话 dispose 顺序、分组路由) |
test-client.mjs |
浏览器半侧结构测试(最小 React 替身) |
License
MIT © 2026 BigPizzaV3