LilJoeIJOYU/dsh-session-delete ↗★ 0
dsh-session-delete
Adds a real Delete action to a Session's sidebar row menu: removes the stored log, the projection cache and (optionally) the subagent logs, then retires the row. | 为侧栏会话行菜单加一个真正删除会话的操作。 适合需要彻底清理会话隐私及磁盘日志、释放存储空间的用户。
같은 패키지 이름의 다른 저장소
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:LilJoeIJOYU/dsh-session-deletedsh-session-delete
Adds the operation the Harness itself does not have: deleting a conversation.
English | 中文
Why this exists
DSH can archive a Session but never delete one. That is a product decision, not a missing button:
dsh-client-ui-workspacerecords the limit — “No Session deletion — sessions can be archived but never deleted … Workspace registration deletion does not delete Sessions.”dsh-session-persistence-jsonlrecords the same at the storage seam — “Nothing deletes session files — logs accumulate underrootuntil removed externally; the seam has no deletion API.”
Archiving only appends the Session id to archivedSessionIds in
$DSH_HOME/storages/workspace.json; the log, the projection checkpoint and the
attachments all stay on disk. This bundle supplies the external removal that the
documentation says is the only way, behind a normal sidebar action.
What it adds
- One row in the Session “...” menu —
sidebar.workspaces.session.menu.itematorder: 500, after the shipped Rename / Fork / Pin / Archive rows, with a group hairline above it and the host's destructive-row colors. - One confirm dialog in
shell.overlay, because the row unmounts with its menu. - One Host Remote,
sessionJanitor/deleteSession, which is the only place the deletion rules live. The row menu is a caller of it, not a second implementation.
What a deletion removes
For the Session and, by default, every subagent Session beneath it:
| Path | What it is |
|---|---|
| `$DSH_HOME/sessions/ | |
| //` | the stored log (session.v4.jsonl.zstd) |
| `$DSH_HOME/sessions/ | |
| /.jsonl[.zstd]` | a pre-release flat sibling |
$DSH_HOME/sessions/.jsonl[.zstd] | the pre-release flat root layout |
$DSH_HOME/storages/session_projcache/sessions/.json | the projection checkpoint |
Then the id is dropped from the archive and pin sets, and api-session/removed is
emitted so every connected Client retires the row immediately.
Left alone on purpose:
- Content-addressed attachments under
$DSH_HOME/attachmentsand$DSH_HOME/cache/attachments— they are shared between Sessions by hash, so deleting one Session's attachments could break another's. - The Session search index — it reconciles against
sessionPersistence.list()on every read, so a removed log drops out of it on its own. workspaces[].sessionIdsmembership — the registry exposes no removal API, and the sidebar derives rows from Session summaries, so a stale id renders nothing.
Refusals
deleteSession returns a result object rather than throwing, and every expected
failure carries a reason:
reason | Meaning |
|---|---|
invalid-id | the value is not a Session id (no separators, so it can never escape sessions/) |
active | the Session is loaded in memory; its log would be rewritten right after removal. Archive it, or switch away and delete it once it is closed |
unknown | no stored Session and no artifact carry that id |
partial | some paths survived (a lock or a permission problem); failed names them |
Layout
| File | Role |
|---|---|
index.js | Host plugin: the sessionJanitor service, its Typert contribution, and the deletion policy |
purge.js | Plain Node library: session-id validation, artifact discovery, measurement, removal |
remote.js | Host wire contract: zod schemas, the invocation descriptor, the Client mount contribution |
typert.host.js | The reflection manifest index.js registers with ctx.typert |
client.js | Browser plugin: the menu row, the confirm dialog, the styles copied from ui-primitives |
test/ | node --test coverage for the removal rules and the Host policy |
remote.js and the contract mirror inside client.js are two copies of one
descriptor: the browser module table has no zod, so the browser half restates the
schemas with a minimal validator. Keep them in sync.
Why index.js registers the reflection itself
There is no "./typert" export on purpose. That export is the Typert loader's own
discovery path, and the loader caches its per-package verdict for the process
lifetime — a bundle installed into a running Harness would sit unregistered until
the next restart. ctx.typert.register(contribution) is the documented path for
“any other owner”, and its disposer withdraws the contribution when the plugin
unloads, so the removal rules and the wire contract mount and unmount together.
Install
Install it as a bundle in the current profile; do not hand-edit the profile's
package.json or run pnpm in the profile directory. Ask the agent for the bundle, or
call plugin_manager directly:
plugin_manager action=install_bundle target=https://github.com/LilJoeIJOYU/dsh-session-delete
Working from a checkout instead? Install the directory:
plugin_manager action=install_bundle target=
A Host plugin module that was already imported cannot be replaced inside the running
process. set_plugin toggles, remove_bundle, and reinstalling all re-import the
same module URL, so a change to any Host file (index.js, purge.js, remote.js,
typert.host.js) becomes live on the next Harness start, not before. The browser
half is separate: an installed bundle's Client artifact loads without a restart.
Test
node --test "test/*.test.mjs"
The tests build throwaway Harness homes; they never touch real Session data.
Limitations
- A
partialoutcome still retires the row: the id is announced as removed even when a file survived. The dialog shows the surviving paths. - A Session that a Client currently holds open cannot be deleted; the Harness exposes no way to dispose one Session without its Agent.
- Cascade reaches subagent Sessions through
ctx.subagents.listDescendants, so a corrupt parent log can hide children that would then be left behind as orphans.
中文
为什么需要它
DSH 只能归档会话,不能删除会话,这是产品决定而不是漏了按钮:ui-workspace 明确
写着「没有 Session 删除」,session-persistence-jsonl 也写着「不删除会话文件
——日志在 root 下累积,直到外部移除;seam 无删除接口」。归档只是在
$DSH_HOME/storages/workspace.json 的 archivedSessionIds 里加一个 id,日志、
投影缓存和附件都还在磁盘上。本插件把文档所说「唯一可行的外部移除」做成侧栏动作。
增加的东西
- 会话「...」菜单里多一行「删除会话」(
order: 500,排在重命名 / 分叉 / 置顶 / 取消归档之后,带分组细线,用 host 的危险色)。 shell.overlay里一个确认弹窗(行随菜单卸载,弹窗不能放在行里)。- 一个 Host Remote
sessionJanitor/deleteSession,删除规则只在这里实现一次。
真正删掉的内容
目标会话及其(默认)全部子代理会话的:会话日志目录、预发布期的扁平日志文件、
投影缓存 storages/session_projcache/sessions/.json。之后把 id 从归档/置顶集合里
摘掉,并发出 api-session/removed,让所有已连接的客户端立刻收掉那一行。
故意不动:内容寻址的附件(多会话按哈希共享)、搜索索引(每次读取都会和
sessionPersistence.list() 对账)、workspaces[].sessionIds 成员关系(注册表没有
移除接口,而侧栏按会话摘要渲染,残留 id 不会渲染出任何东西)。
拒绝的情形
invalid-id(非法 id)、active(会话还在内存里,删了会被立刻重写)、
unknown(没有任何记录带这个 id)、partial(有文件被占用,failed 里列出路径)。
安装
让 agent 装,或直接调 plugin_manager:
plugin_manager action=install_bundle target=https://github.com/LilJoeIJOYU/dsh-session-delete
在本地 checkout 上开发时,指向目录即可(不要手改 profile 的 package.json,也不要在
profile 目录里跑 pnpm):
plugin_manager action=install_bundle target=
已经被导入过的 Host 插件模块无法在运行中的进程里替换。 set_plugin 开关、
remove_bundle、重新安装都会重新导入同一个模块 URL,所以对任何 Host 文件
(index.js、purge.js、remote.js、typert.host.js)的修改要在下次启动 Harness
才生效。客户端半边是独立的:装好的 bundle 的浏览器产物不需要重启就能加载。
另外本插件故意不导出 "./typert":Typert loader 会把它对每个包的判定结果缓存一整个
进程生命周期,正在运行的 Harness 里新装的 bundle 要等到重启才会被注册。因此
index.js 自己调用 ctx.typert.register(contribution) —— 这是文档给「其他 owner」的
路径,返回的 disposer 会在插件卸载时撤销贡献。