Geralt4
dsh-plugin-session-delete
DSH plugin: permanently delete a session's on-disk log and attachments from the Web UI (header button + confirm dialog).
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 17, 2026
- Updated
- Aug 17, 2026
Introduction
dsh-plugin-session-delete
A DeepSeek Harness plugin that adds a real, permanent delete for sessions (chats) to the Web UI — a "Delete session" button in the conversation header plus a confirm dialog. On confirm it removes the session's on-disk conversation log and attachments and hides the row from the sidebar, without a page reload.
Today the stock UI can only archive a session (hide it; the log stays on
disk forever). Core has no deleteSession RPC, so this plugin adds a small
host-side HTTP route that performs the durable deletion, plus a client-side
header action that calls it.
⚠️ Irreversible. Deleted sessions cannot be restored. Stop any running agent on a session before deleting it (the route also best-effort cancels the agent). Works in the Web profile and the desktop client.
Install
Into a dsh web profile (the dsh plugin command forwards to pnpm in the
profile directory), then add the Loader entry row and restart the profile:
# from npm
dsh plugin --profile web add dsh-plugin-session-delete
# or directly from GitHub (lib/ is committed, so no build needed)
dsh plugin --profile web add github:Geralt4/dsh-plugin-session-delete
Then append the plugin's Loader entry to the profile's cordis.patch.yml
($DSH_HOME/profiles/web/cordis.patch.yml) and restart dsh web:
- insert:
- id: session-delete
name: dsh-plugin-session-delete
After a page refresh, a Delete session button appears in the conversation header.
How it works
┌──────────────── browser ────────────────┐ ┌──────── host (Node) ────────┐
│ conversation.session.header.actions slot│ │ webServer.register(exact) │
│ DeleteSessionHeaderAction (button) │──> │ POST /api/session-delete │
│ DeleteSessionDialog (confirm modal) │ │ { sessionId } │
│ │ confirm() │ │ │ │
│ │ fetch POST /api/session-delete │ │ ▼ │
│ ▼ │ │ cancel agent (best-effort) │
│ on ok: ctx.workspaces.archiveSession() │ │ sessionPersistence.inspect │
│ (row disappears from sidebar now) │ │ → absolute log path │
└─────────────────────────────────────────┘ │ fs.rm(dir, recursive) │
│ (log + attachments) │
│ SQLite FTS rows auto-drop on │
│ next persistence observation │
└─────────────────────────────┘
Why a plugin and not a core PR
DeepSeek Harness core has no session.delete / workspace.deleteSession RPC
(only workspace.archiveSession, which hides, and session.cancel, which
stops). The static UNARY_ROUTES table in dsh-host-apiproxy cannot be
extended by a plugin, so a plugin cannot add a routed RPC. It can, however,
register its own HTTP route via the webServer service
(dsh-host-webserver) and reach the same on-disk result through the public
sessionPersistence.inspect(id).path. This plugin does exactly that.
This is a best-effort approximation of what really belongs in core (the
persistence coordinator owns live-session retirement and exact path encoding).
If maintainers ever add a first-class deleteSession, this plugin can be
reduced to a thin UI wrapper around it.
Project layout
dsh-plugin-session-delete/
package.json # dsh.client manifest + peerDeps + tsdown scripts
tsdown.config.ts # two-entry build (host + client)
tsconfig.json
src/
index.ts # host half: webServer route + fs.rm
client.ts # browser half: header action + confirm dialog
lib/ # build output (index.js + client.js)
Build
pnpm install
pnpm bundle # tsdown -> lib/index.js, lib/client.js (+ .d.ts)
Client bundle format note
The browser loads client halves via window.__ModuleLoader__.load({ id, factory })
(see @deepseek-ai/dsh-client-modules). In the DSH monorepo a shared tsdown
plugin wraps the client entry into that form; the host entry is plain ESM and
builds unchanged.
- In repo (recommended for first-time development): drop this package under
packages/(e.g.packages/plugin/session-delete) and copypackages/session-query/session-log-export/tsdown.config.tsso the client output is wrapped. Add the package to thewebprofile's bundle list. - Out of tree: either copy that in-repo tsdown config, or author
src/client.tsdirectly in the factory form so a plain externalised build is loadable (seeOut-of-tree buildbelow). The peer deps intsdown.config.tsmust stay external so the loader'srequireresolves them at runtime.
Install into a profile
Out-of-tree plugins are added to a profile with the dsh plugin command, which
forwards to pnpm in the profile directory:
# from a checkout of this plugin, or an npm tarball
dsh plugin --profile web add <path-or-tarball-of-this-package>
dsh plugin --profile web install
Then rebuild the profile's frontend artifacts and refresh the running URL (the
shell is not a standalone app; dsh web injects window.__DSH_BOOT__).
To develop against the running GUI at http://127.0.0.1:3080: the client half
reloads without a refresh only while pnpm run dev:web is also running from
the same checkout to rebuild client bundles. The host half and the shell
require a rebuild + refresh.
Run / verify
- Start the web profile:
dsh web(orpnpm dsh webfrom a source checkout). - Open a session. A "Delete session" button appears in the conversation header.
- Click it → confirm dialog → "Delete permanently".
- The session's on-disk directory under the persistence root is removed and the row disappears from the sidebar.
The on-disk session log lives at
<persistenceRoot>/<projectKey(cwd)>/<encodeSegment(id)>/session.jsonl[.zstd]
(+ co-located attachments). This plugin never recomputes that path: it reads
the absolute path from sessionPersistence.inspect(id) and removes that
artifact's directory.
Out-of-tree build (client factory form)
If you cannot use the in-repo tsdown client plugin, replace src/client.ts
with the factory-form equivalent so a plain externalised ESM build is loadable.
The shape is (mirroring the published @deepseek-ai/dsh-session-log-export/lib/client.js):
window.__ModuleLoader__.load({
id: "dsh-plugin-session-delete",
factory: (require) => {
const { jsx, jsxs, Fragment } = require("react/jsx-runtime");
const { createSnapshotStore } = require("@deepseek-ai/dsh-client-runtime/client");
const { Modal, Button, IconTrashOutline16 } = require("@deepseek-ai/dsh-client-ui-primitives");
// …the controller, dialog, header action, and apply/inject from src/client.ts…
return { apply, inject };
},
});
Keep react, react/jsx-runtime, and @deepseek-ai/* external in
tsdown.config.ts so these require(...) calls are resolved by the loader at
materialisation, not bundled.
Limitations
- Live sessions: if an agent is actively writing when you delete, the persistence coordinator may recreate the log after this plugin removes it. The route best-effort cancels the agent first; the dialog warns the user.
- Descendants: this v1 deletes one session's directory only. Subagent
children are not cascaded. (Inspect
session.export'sincludeDescendantspattern if you want a cascade follow-up.) - Workspace accounting: the row is hidden via
workspace.archiveSession, which keeps the id in its workspacesessionIdsslot. Stale accounting is reconciled by core on the next persistence observation. True removal from accounting belongs in core. - SQLite FTS index: dropped automatically by core's reconciliation once the log file is gone; this plugin does not touch the index directly.
License
MIT