dsh-dialogue-cleanup
English · 简体中文
Delete DeepSeek Harness sessions from the running Web GUI — without restarting, without leaving the window.
One row in the session ... menu for a single delete, plus a bulk management page inside Settings.
Bulk management panel in Settings: search, filters, per-row status badges and 16 session rows
Install ·
Gallery ·
How it works ·
Releasing ·
Releases
Why it exists
One-line positioning: deleting a session should actually delete it. DeepSeek Harness keeps a session
in four places at once — the live store, the workspace records, the registry leftovers
(pin / archive), and the files on disk. Removing one of them leaves a ghost row, a stale
workspace, or a directory that silently keeps occupying disk. Everything below is that
positioning made concrete:
- One irreversible operation, implemented once.
deleteSession lives in the Host
(lib/service.js) and is reached through two thin adapters — the HTTP contract and the slot UI —
so the single-delete path and the bulk path cannot drift apart.
- A running session is refused, not guessed at. The Host decides
deletable and the UI simply
does not offer what the Host would reject. The panel never decides by itself whether something is
safe to delete.
- Archived sessions are still shown by default. Archiving is not deleting: an archived session
still occupies disk, so hiding it by default would hide exactly the files you came to clean up.
(The official sidebar hides them; here it is a tri-state filter —
hide archived / all /
archived only — and archived are shown by default.)
- Subagent rows are labelled. The official sidebar hides
origin: 'subagent' rows entirely, so
whenever those rows do appear (kind = subagent / all) each one is explicitly badged — a subagent
session must never look like an ordinary conversation.
- It says what it actually did. The result line reports ok / failed / degraded / directories
removed, and each failure keeps its own reason verbatim. When the live store entry cannot be
detached, the plugin reports
detached: false rather than claiming success.
- Errors are semantically truthful. Unlike the upstream package (which always answered
200),
this one uses real status codes: 400 / 403 / 404 / 405 / 409 / 429 / 500.
UI at a glance
The confirmation overlay: irreversible, and it says so. Two closing paths only — an explicit
Cancel button, or Esc. Clicking the scrim deliberately does not dismiss it, because a
mis-click on an irreversible delete should not cost you your chance to confirm:
Delete confirmation: permanent deletion warning, session id, and Cancel / 永久删除 buttons
The overlay is a native <dialog> driven by showModal(), so it enters the browser's top
layer and therefore stacks above the Settings panel it was opened from (a plain positioned element
could not). That also brings a real focus trap and correct Esc semantics for free — and it needs no
portal outside the component tree.
The bulk panel (inside Settings) — search, project / state / session-kind / archive filters,
sort, paging, and per-row status badges. The badges in the screenshot above come from real render
output: 空闲 (idle) / 运行中 (running, not selectable) / 子代理 (subagent) / 已归档 (archived).
The two screenshots are generated from the actual component tree and the actual CSS by
scripts/render-shot.mjs (the same React stand-in the render tests use), rasterised with headless
Edge. They are not hand-drawn mockups; they are also not a photograph of a live GUI — see
Honest limitations.
Quick Start
dsh plugin --profile web add dsh-dialogue-cleanup
- Single delete — hover a session row in the sidebar, open the
... menu and pick
Delete session (registered at order: 500, right after the shipped Archive entry at 400).
A themed confirmation appears; confirming removes the live entry, the workspace records, the
registry leftovers and the on-disk artifacts.
- Bulk delete — open Settings → Dialogue Cleanup. Tick rows (or Select all deletable),
then delete them in one request.
- Host-side changes need a restart of the DSH process; client-side changes do not (see the table
in docs/RELEASING.md).
Compatible with DSH 0.2.0-rc.2. The npm package and the GitHub Release are published together;
versioning rules and the release chain live in docs/RELEASING.md.
How it works
One npm package carries both halves, installed into the host per the official plugin spec:
- Host half (repo process) —
lib/index.js is a Cordis function plugin exporting only
name / inject / apply. It registers a single ctx.webServer prefix route and keeps the
entire deletion implementation in lib/service.js. Filesystem removal and path containment live in
lib/artifacts.js; the audit trail in lib/audit.js.
- Client half (browser) —
client.js, a single-file bundle registered through
window.__ModuleLoader__.load({ id, factory }), with zero dependencies beyond require("react").
- Both entry points are official slots; neither touches the DOM:
sidebar.workspaces.session.menu.item at order: 500 → the single-delete row.
settings.section (id: dialogue-cleanup, order: 25, after the shipped
account(-10) / general(0) / models(10) / plugins(15) / agent-presets(20)) → entry and panel.
One registration is one Settings page, whose content renders in the panel's content column.
- Because the panel lives in Settings it needs no
layout service and does not compete for space
in main or the sidebar — one fewer optional dependency and one fewer degradation branch.
- The stylesheet travels with the component. The menu item renders its own
<style> beside
itself (finding H1 of the project's own audit: the row was rendering unstyled because the
stylesheet was only injected on the settings path).
The deletion sequence
- Refuse a running session — 409
refused.
- Detach the live store entry — otherwise the deleted row survives as a ghost.
- Drop workspace records — counting only workspaces that actually detached.
- Clear registry leftovers — pin / archive records, via the public idempotent API.
- Remove on-disk artifacts —
<DSH_HOME>/sessions/<project>/<sessionId>, plus the derived
projection cache.
- Write an audit line —
<DSH_HOME>/logs/dialogue-cleanup.jsonl.
HTTP contract
| Method | Path | Purpose |
|---|
POST | /api-ext/dialogue-cleanup/delete | { "sessionId": "..." } → delete one; { "sessionIds": [...] } → delete many (the Host runs the same deleteSession per id and returns a per-id result) |
POST/GET | /api-ext/dialogue-cleanup/list | session rows with deletable / blockedReason |
GET | /api-ext/dialogue-cleanup/status | capabilities, config, runtime paths |
Envelope: { ok: true, value } / { ok: false, error: { code, message, hint? } }.
Configuration
All tunables live in the plugin Config (lib/config.js), so a user's patch layer survives
upgrades. A matching override replaces the whole config — write every field you care about.
| Key | Default | Meaning |
|---|
maxBodyBytes | 65536 | request-body ceiling (upstream: 1 MiB) |
readTimeoutMs | 10000 | body-read timeout (upstream: none) |
audit / auditPath | true / null | audit log; null → <DSH_HOME>/logs/... |
rateLimitPerMinute | 60 | per-minute delete request budget (a batch counts once); 0 disables |
requireIdleSession | true | safety valve for the running-session refusal |
stopIdleAgent | false | see the honest-degradation note below |
matchAlternateIdSpelling | true | probe the other session- spelling, guarded |
removeProjectionCache | true | also drop the derived projection cache |
clearRegistryRecords | true | unpin / unarchive leftovers |
confirmInUi | true | UI confirmation (the Host does not trust it) |
titleCacheTtlMs | 300000 | title-fold cache TTL; 0 disables |
titleFetchConcurrency | 8 | shard concurrency when folding titles |
titleLimit | 100 | max rows per page to fold titles for; 0 skips title reading |
runningProbeTtlMs | 1000 | how long a running-state snapshot is reused. Turns batch delete from O(N) full-corpus list calls into O(1); 0 re-probes every item |
Honest limitations
These are measured against the live runtime and differ from what the upstream package assumes.
They are reported truthfully rather than papered over:
- No public stop for an idle agent. The public
Agent type is
{ readonly id: SessionId } — no status, no ctx. Upstream's agent.status === "running" and
agent.ctx.fiber._unload() therefore cannot be relied on (_unload is a Cordis private method
and is deliberately never called). "Is it running?" comes from the public
sessionController.list() → SessionSummary.running. A session that still has an attached agent
and cannot be stopped safely is refused, and the UI shows why.
- Detaching the live entry is best-effort. The public
sessions service exposes
create/prepare/enter/announce/flush/get/list/fork — no public detach. The plugin tries the
public method first, then the internal store entry, and reports detached: false when neither is
available instead of claiming success.
- Two on-disk id spellings coexist. Observed in one
<DSH_HOME>:
...\<uuid>\session.v4.jsonl.zstd and ...\session-<uuid>\session.v4.jsonl.zstd. An exact-id join
can never delete the prefixed half. This plugin probes the alternate spelling only when it can
confirm the candidate is not a different live session; with no such evidence it deletes nothing
rather than risk a neighbour.
- The screenshots are real render output, not a live-GUI photograph. They come from the actual
component tree + actual CSS, rasterised headless; the theme tokens are pinned to a light theme and
the row data is a demo fixture. What the panel looks like inside a running GUI with the real theme
has not been captured here.
Development and acceptance
# Full gate: node --check + migration/contract/render/release regression suites
node --test "test/**/*.test.mjs"
# Release gate only (no version change, no packing, no publishing)
pwsh -File scripts/release.ps1 -LocalOnly
# Regenerate the two documentation screenshots from the real component tree
node scripts/render-shot.mjs
The suites are currently 161 tests across 9 files:
| Suite | Covers |
|---|
contract.test.mjs | the host ↔ client contract and slot declarations (largest suite) |
service.test.mjs | deleteSession end-to-end behaviour and refusals |
client.test.mjs | registration surface, ids, no-cross-package requires |
render.test.mjs | components actually rendered through a React stand-in |
artifacts.test.mjs | path containment, symlink refusal, alternate-spelling guards |
session-id.test.mjs | id validation (no separators or dots) |
eol.test.mjs | line endings, and that every tracked file is git-declared eol=lf |
release-workflow.test.mjs | the OIDC publishing structure (reverse assertion: pack-release.yml must not contain its own publish steps) |
ps-script.test.mjs | PowerShell 5.1 BOM / encoding pitfalls |
Two of these exist because the corresponding bug actually shipped and was caught afterwards:
eol.test.mjs — the first release produced different bytes from CI (pnpm pack) and from
local (npm pack): icon.svg and LICENSE were CRLF in CI, because .gitattributes declared
only * text=auto and the GitHub Windows runner defaults to core.autocrlf=true. The first
version of this test missed it (it scanned a hardcoded extension list, so *.svg and the
extension-less LICENSE were invisible); it now sweeps every git-tracked file and reads the
effective attribute via git check-attr. Fixing it also caught .gitignore.
release-workflow.test.mjs — the OIDC trust is bound to a workflow filename, and writing
the publish steps a second time is exactly the bug that broke that other repository's releases
twice. See docs/RELEASING.md.
Repository layout
| Path | Contents |
|---|
lib/index.js | apply(ctx, config) — assembles and registers the host routes |
lib/service.js | deleteSession — the single deletion implementation |
lib/artifacts.js | disk removal + path containment |
lib/http.js | same-origin checks, body limits, status codes |
lib/config.js | Config schema + defaults |
lib/audit.js | JSONL audit trail |
client.js | the web client half: slot registrations (menu row + confirm overlay + settings section) |
cordis.patch.yml | inserts the host half, inject: [webServer] |
.github/workflows/ | pack-release.yml (tag → gate → pack → GitHub Release) and publish-npm.yml (the only npm-publish implementation, reused via workflow_call) |
docs/RELEASING.md | version rules and the release chain |
scripts/release.ps1 | version bump + gate (+ CI pack/release) |
scripts/render-shot.mjs | regenerates the documentation screenshots |
test/ | the 161-test regression suite |
License
MIT