dsh-session-delete: Adding session deletion to DSH Web interface

Foreword

DSH’s session logs are a directory on disk, but DSH itself lacks deletion capabilities: SessionPersistence only has create/open/flush/stat/list, and WorkspaceRegistry only has archive/unarchive. Archiving is just a soft deletion; log directories, registry entries, and subagent logs remain on disk. Over time, sessions accumulate without a real entry to clean them up from the interface.

dsh-session-delete is a third-party plugin that solves this problem. The maintainer is Neruviko. It belongs to the client-side plugin category and currently has 0 stars on the community directory SkillHub. It adds “Delete Session” to the sidebar session row’s “…” menu, complete with a confirmation dialog and backup guidance. Below is an introduction to what it does, how to install it, the security design of the deletion process, and usage considerations.

What is this

In one sentence: In the DSH Web interface, completely delete a session along with its disk logs and its subsession subtree, and clean up the workspace registry.

The target runtime environment is the DSH 0.2.0-rc.2 Web GUI (verified usable by the author). Node version requirements are ^22.19.0 || >=24.0.0, consistent with DSH itself. The plugin package version is 0.2.2, licensed under MIT. Both the Host and Browser halves are hand-written with no build steps.

Core Features

The feature list comes from the plugin README and has been verified on GitHub and the SkillHub directory page:

  1. Added “Delete Session” to the “…” menu of any session row in the sidebar, positioned after “Archive Session”.
  2. Can delete any session, including running or open sessions; before deletion, a stop event is dispatched, stopping its running work (rounds, subagents, tasks, and reminders are all cancelled).
  3. When deleting a parent session, the entire subsession subtree is deleted along with it. Subsessions are invisible in the sidebar and cannot be deleted individually; deleting only the parent leaves orphan logs that can never be accessed again.
  4. The confirmation dialog shows session title, creation time, log size, and number of subsessions; when there are subsessions, it explicitly states “Will also delete N subsessions and their descendants”.
  5. Embedded “Download this session’s ZIP archive” link in the dialog to guide backup.
  6. Must check the confirmation box to click delete.
  7. After deletion, the sidebar row disappears immediately without needing to refresh the page.

Worth noting separately is that it does not perform hidden enumeration: the session directory is not enumerated, but is deterministically calculated from the JSONL backend’s directory layout:

<DSH_HOME>/sessions/<projectKey(cwd)>/<encodeSegment(sessionId)>/session.v4.jsonl.zstd

index.js replicates two path transformation functions character-by-character, encodeSegment (encodes any string into a single safe path segment) and projectKey (converts cwd to project directory name, folds separators into -, adds -- at both ends, e.g., /home/me/projects/demo → --home-me-projects-demo--). The subtree comes from DSH’s own traceSession(), or the parentSession link in the header of sessionPersistence.list(), neither touching disk directory enumeration.

Installation and Enablement

Below is the installation command given in the README. It is a hand-written git+https form, not dsh plugin add github:owner/repo. Install to the web profile (other profiles are similar, just replace web):

dsh plugin --profile web add git+https://github.com/TonyZiYuan/dsh-session-delete.git

dsh plugin add forwards to pnpm. After installation, it automatically appends the newly installed bundle to the profile’s dsh.profile.bundles (based on the dsh.bundle field in the package), no need to manually edit package.json.

If installation fails saying a dependency’s build script was intercepted, pnpm defaults to not running the prepare script of installed dependencies, but git dependencies need to be built first. At this point, follow the prompt to add the package to the profile’s pnpm-workspace.yaml (located in ~/.dsh/profiles/web/):

packages:
  - .

allowBuilds:
  dsh-session-delete: true

Then re-run the installation command above. pnpm >= 11 merged settings like onlyBuiltDependencies into this allowBuilds map. See the pnpm v10 -> v11 migration guide for details.

Update and uninstall:

dsh plugin --profile web update dsh-session-delete
dsh plugin --profile web remove dsh-session-delete

Can also install from a local directory; the path must be absolute:

dsh plugin --profile web add link:/absolute/path/to/dsh-session-delete

The relative path for link: is resolved relative to the profile’s manifest directory (i.e., ~/.dsh/profiles/web/). Writing ./dsh-session-delete would point to ~/.dsh/profiles/web/dsh-session-delete and fail to install.

After installation, you must restart dsh web. The bundle and browser halves are only loaded at startup (patchReload: live can only hot-update patches, not new browser code). Use this to confirm successful mounting:

dsh --profile web --dump-config   # Should see the `dsh-session-delete` entry

Typical Usage

The plugin has no command-line entry point; once installed, all operations are in the Web interface. The deletion flow of a session, in the order described in the README:

  1. Stop running work. Dispatches workspace/session-stop (same event as DSH’s stopActivity: true during archiving). Providers cancel the session’s rounds, subagents, tasks, and reminders, then wait briefly for cancellation to take effect. Provider errors are only logged, not blocking deletion.
  2. Flush barrier. Calls sessionPersistence.flush() to ensure no buffered events. Failure does not block; subsequent whitelist checks still hold.
  3. Locate directory. Calculates directory from cwd in the header; does not scan the sessions root directory.
  4. Enumerate descendant subtree. Uses Session Query’s traceSession() to get subsession trees of any depth; falls back to the parentSession link in the header of sessionPersistence.list() if the query service is unavailable. Defaults to deleting the whole subtree.
  5. Whole tree pre-check. Locates directory for parent session and each descendant one by one, checks if it escapes the sessions root, and validates content whitelist. Proceed only if all pass; if any fails, abort entirely without deleting anything.
  6. Recursive delete. Calls fs.rm(dir, { recursive: true, force: true }) one by one. When blocked by OS handle locks (Windows EPERM/EBUSY, POSIX EACCES/ETXTBSY), returns session/in-use, prompting to close the session and retry.
  7. Clean registry. For each deleted ID, executes unarchiveSession / unpinSession / detachSession on workspaceRegistry, while updating memory cache, persistence table, and browser stream pushes. Falls back to atomic rewrite of workspace.json (temp file + rename, keeping a .bak) if registry composition is unavailable. Failure in this step does not block deletion, only marks registryCleaned: false in the response.
  8. Publish removal announcement. Sends api-session/removed for each deleted ID. DSH’s existing forwarding chain will remove that row from all connected browsers’ sidebars. Without this step, the row will persist until the next refresh.
  9. Record in tombstone. The deleted ID (including the whole subtree) is recorded in the current Host generation’s memory tombstone set for the Browser half’s “Ghost Reaper” to use.

At the HTTP level, there are two routes, and write requests additionally require the origin to be the loopback address:

Method Path Action
GET /api/session-delete/sessions List of deletable sessions + tombstones (IDs deleted in this Host generation, for the Reaper)
POST /api/session-delete/delete Delete, body {sessionId, cascade?}, cascade defaults to true

The deletion response contains a descendants summary for easier confirmation of cascade results:

{
  "ok": true,
  "deleted": true,
  "absent": false,
  "freedBytes": 8192,
  "registryCleaned": true,
  "descendants": { "requested": 3, "deleted": 3, "ids": ["child-1a", "child-1b", "grand-1a"] }
}

Both routes are wrapped in the ctx.connection.requestRejection() trust fence (Host/Origin spoofing and unauthenticated requests will be rejected). This is explicitly stated in the README and can be verified directly.

A Gotcha: Ghosts Revive After Refresh

This is a relatively critical note in the README. For chat sessions, the Agent/Session object resides permanently in Host memory (DSH’s design ensures idle sessions can be continued). Deleting destroys the disk logs, but there is no public API to destroy live session objects held by other fibers. The client list baseline merge for live sessions is unconditional (listSessions from dsh-session-query). Therefore, after refreshing the browser, the baseline rebuilds from memory, and deleted rows come back to life: appearing under “Unassigned” (registry members are cleaned), and clicking them still shows complete text (history reads from live first, not touching disk).

The plugin uses a two-layer coordination to hedge against this:

  • Host side: Every successful deletion records the ID in the memory tombstone set, and the GET /sessions response carries a tombstones array. The tombstone and the ghost die together; neither exists after a Host restart.
  • Browser side: Subscribes to the client session list snapshot. If an ID is “in the tombstone but appears in the list”, it automatically sends a deletion request again, taking the absent idempotent path, triggering registry cleanup and re-announcement, causing the row to drop immediately. It only harvests IDs within the tombstones; new blank sessions are not affected.

The effect is that even if a ghost flashes after a refresh, it is collected within sub-second. The README also explicitly states that the millisecond-level flash window cannot be rooted out at the plugin layer and requires upstream support for “excluded deleted sessions” in baseline merging. The manual fallback method is to click “Delete Session” again on the ghost row; the dialog will show “Logs no longer on disk” status, and confirming will clear it.

Security Design

The security design listed in the README is transcribed directly below:

  • Directory must be inside the sessions root, and basename must equal encodeSegment(sessionId), verified for both parent and each descendant.
  • SessionId must match /^[A-Za-z0-9._-]{1,128}$/.
  • Content whitelist: files placed in the session directory by the user are not objects this plugin should destroy; better to reject.
  • Cascade is all-or-nothing: the entire subtree is pre-checked first; if any member fails, nothing is deleted.
  • Stop before delete: dispatches workspace/session-stop before deletion to avoid tearing logs under active writers; when truly blocked by handles, the OS itself rejects it, mapped to session/in-use.
  • Circular lineage: carries a seen set when enumerating descendants to prevent infinite recursion for malformed lineages.
  • Atomic registry write (fallback path only): temp file + rename; crash won’t truncate workspace.json, and a .bak is kept.

The plugin explicitly does not do: delete attachments directory (content-addressed shared storage, multiple sessions may reference the same image), batch delete, recycle bin/soft delete, modify @deepseek-ai/* core, or full scan of sessions root.

Applicable Scenarios and Notes

Suitable for users already using the dsh web profile with long-term accumulated session logs who need true cleanup instead of archiving. It depends on multiple internal DSH contracts (sessionPersistence’s public root field and list/stat/flush, encodeSegment/projectKey directory layout, workspace/session-stop and api-session/removed events, workspaceRegistry’s unarchiveSession/unpinSession/detachSession, sessionQuery’s traceSession() descendant tree shape, and the Browser half’s slot names and exports from dsh-client-ui-primitives), all verified on 0.2.0-rc.2. Do not assume compatibility on other upstream versions before verification. If deletion starts reporting session/absent for existing sessions or missing dialog components after upgrading DSH, it is likely that some contract has changed. Failure is safe direction (better to refuse deletion than to mistakenly delete). Just update the path functions at the top of index.js or component usage by comparing with the new source code.

A few usage notes:

  1. Plugin runs with the permissions of the current dsh web process. Read index.js and client.js before installing to confirm no objection in audit. License is MIT.
  2. Path transformation functions are replicas; if DSH upgrades and changes them, they must be updated synchronously, otherwise deletion will falsely report session/absent.
  3. Deletion is irreversible; the plugin itself does not provide a recycle bin. To keep archives, download the ZIP in the confirmation box first.
  4. Subsessions are cascade deleted by default; if you truly only want to delete the session itself, pass cascade: false (in HTTP body), but the default is to leave no orphans.
  5. Directory proof is source: The SkillHub directory page returns very little content, providing no verifiable feature list or installation commands. The facts in this article are based on the GitHub repository README and package.json. The installation command comes from the original README text and points to a repository address inconsistent with the plugin name (the command points to TonyZiYuan/dsh-session-delete), please verify repository ownership yourself before installing.

Conclusion

The value of dsh-session-delete lies not in adding a button, but in filling in the “Delete” layer for DSH, while having cascade deletion, registry cleanup, and entry control for irreversible operations designed in. A non-ritualistic, complete stop-and-delete is more impressive than just an extra menu item.

Directory page: https://www.skillhub.cn/plugins/Neruviko/dsh-session-delete
GitHub: https://github.com/Neruviko/dsh-session-delete

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye