dsh-workspace-cleaner
Workspace and session cleanup for DeepSeek Harness — a settings page that lists every workspace registration with its session count, plus a delete session row in the sidebar's session menu that really removes the log (into a recoverable trash).
Git 版说明见 README.zh.md。
Why this exists
DSH deliberately ships no session deletion. The JSONL persistence backend documents it plainly:
Nothing deletes session files — logs accumulate under
rootuntil removed externally; the seam has no deletion API.
So the sidebar's session menu only offers Archive. Once a few hundred sessions pile up, the sidebar becomes unmanageable and the logs keep growing on disk. This plugin fills the two gaps:
- Settings → Workspace Cleanup — one table of every workspace: its path, session count, whether the directory still exists, bulk archive of its sessions, and deletion of the workspace registration.
- Session menu → Delete session — moves that session's log directory into a trash folder, hides the sidebar row immediately, and offers restore/permanent deletion from the settings page.
Nothing is unlinked on the first click: deletion is a rename, so it stays recoverable.
Install locally
# from the directory that contains this package
dsh plugin add /absolute/path/to/dsh-workspace-cleaner
Or, inside a DSH session, ask the agent to install the bundle from the local directory; plugin_manager install_bundle links the checkout into the profile (node_modules/dsh-workspace-cleaner), so later edits to the files need only a restart.
Restart DSH after installing (the Host half registers HTTP routes), then reload the Web GUI.
Use
Session menu
Open a session's "…" menu → Delete session → confirm.
- Deleting a live session is refused: its writer still owns the log. Switch to another session first.
- The row disappears immediately — deletion archives the session (what the sidebar filters on) before moving the log, because the registry's in-memory session index would otherwise keep listing the file until a restart.
- The log directory is renamed to
<dsh-home>/session-trash/<timestamp>--<session-id>/with ameta.jsonrecording the title, project directory, cwd and deletion time.
Settings → Workspace Cleanup
| Action | Effect |
|---|---|
| Archive sessions (per workspace / bulk) | workspaceRegistry.archiveSession — sessions leave the grouped list, recoverable from the archived list |
| Delete workspace (per workspace / bulk) | workspaceRegistry.delete — drops the registration only; session logs, directories and files stay |
| Trash → Restore | Moves the log back, unarchives the session and re-attaches it to its workspace |
| Trash → Delete permanently | Removes the trashed directory for good and reports the freed bytes |
How it works
client.js ──POST /workspace-cleaner/api/{list,apply,trash,deleteSession,restore,purge}──► index.js
(settings.section page + sidebar session menu row) (Host: workspaceRegistry,
sessionPersistence,
node:fs over the log root)
- The Host half owns every operation and exposes them over one JSON route guarded by the same browser-trust fence as the shipped browser routes (
ctx.connection.requestRejection). It is the only part that touches the filesystem. - The Client half registers two slots: a
settings.sectionpage (Workspace Cleanup) and one row insidebar.workspaces.session.menu.item(Delete session, order 500, after the shipped rows). All text goes through the locale service (zh/en), and all styling uses--dsw-alias-*theme tokens. - Data comes from the Host rather than the Client's
ctx.workspacesface, because that face can only mutate a workspace it is already given — it cannot enumerate them for a plugin page.
Session log discovery
The JSONL backend's documented layout is:
<root>/
--<normalized-cwd>--/
<session-id>/
session.v4.jsonl.zstd
<root> is resolved from the first candidate that exists and holds project directories:
config.sessionRoot(see below)$DSH_PROFILE_DIR/../../sessions$DSH_PROFILE_DIR/sessions$DSH_HOME/sessions~/.dsh/sessions
A session is matched by its own directory name, and the directory must contain session*.jsonl[.zstd].
Configuration
- id: workspace-cleaner
name: dsh-workspace-cleaner
config:
sessionRoot: 'D:\somewhere\session-logs' # optional override
| Field | Default | Meaning |
|---|---|---|
sessionRoot |
auto-discovered | Absolute path of the JSONL session-log root |
Safety model
- A live Agent (
ctx.agents.get(id)) is refused with409 session/live. - A missing log is not an error: the session was either never materialized (nothing on disk) or already deleted; the accounting is cleaned and the caller gets
unmaterialized/alreadyDeletedinstead of a failure. - A failed rename rolls back the archive, so the row never claims a deletion that did not happen.
- Trash entry ids are validated (no
.., no separators) before any path is built. - Deleting the currently open session is refused — it is always live.
Known limitations
- A session deleted while its row is still cached in the GUI needs a page reload/restart to drop from the archived list; the grouped list updates immediately.
- The SQLite search index can hold a stale hit for a deleted session until the next DSH start.
- Session directories are matched by id; ids containing characters the backend escapes differently would report "no session log on disk".
- The trash lives beside the log root (
<dsh-home>/session-trash) so the backend never scans it.
Development
No build step: plain ESM, plain browser module.
node .verify-host.mjs # Host route + real filesystem (delete/restore/purge) — 58 checks
node .verify-client.mjs # both slot components under a tiny hook renderer — 67 checks
License
MIT
No comments yet. Be the first to write one.