Skip to main content
F

dsh-delete-chat

fufu1437/dsh-delete-chat

Permanently delete a DeepSeek Harness conversation and every local copy of its data: session log, derived caches, workspace index entries, spill files, and unreferenced attachments.

Install

dsh plugin --profile web add github:fufu1437/dsh-delete-chat

README

Conversation Deletion (@fufu1437/dsh-delete-chat)

A DeepSeek Harness (DSH) plugin that permanently deletes one conversation together with every local copy of its data.

Every Session row in the sidebar gains a "Delete conversation…" entry in its … menu. It opens the confirmation at once — no preview request, no artifact inventory, no byte totals — and confirming runs the deletion in the background: the dialog closes immediately and the sidebar entry disappears when the Host broadcasts the removal.

中文文档:README.zh.md

What gets deleted

A conversation leaves more on disk than most people expect. This plugin clears each of these:

#ArtifactLocationNotes
1Session log directory~/.dsh/sessions/<project-key>/<session-id>/Every on-disk format generation (session.v3.jsonl.zstd, session.v4.jsonl.zstd, …) plus the write lease session.lock
2Projection cache record~/.dsh/storages/session_projcache/sessions/<id>.jsonThe cached title and first-prompt text, including .json.bak.* copies
3Legacy feedback sidecar~/.dsh/storages/message_feedback.jsonA pre-release format the current service no longer reads; it can hold user-written notes
4Workspace index~/.dsh/storages/workspace.jsonAccount order, archive set, and pin set, written through the workspace registry's own API
5Spilled tool output<spill root>/session-<first 12 hex of sha256(id)>/Every local spill root, including $TMPDIR/dsh-spill-*
6Attachments~/.dsh/attachments/v1/{objects,files,file-objects,request-images}Removed only after proving no surviving session references them
7Subagent sessionsAs 1–5Every durable subagent session the conversation spawned, recursively
8DeepSeek upload cache~/.dsh/llm-deepseek/files-v3.jsonRecords of deleted attachments plus the request-image derivatives their variantIds name

Afterwards the plugin broadcasts api-session/removed to every connected client, so the sidebar row disappears immediately instead of waiting for the next list refresh.

Safety model

Deletion is irreversible, so the implementation follows two hard rules.

1. A conversation that is generating is left alone; one that is merely open is overwritten before it is unlinked. The Host keeps an Agent and an open log write handle for every conversation opened in this process, and DSH exposes no public "release this session" API, so a plain rm under a live writer would leave the bytes readable in the orphaned inode — a fake deletion. Therefore:

  • the conversation is generating a response (a turn is in flight) → refused with 409 session-running; stop it first;
  • it is open in this process but idle → refused once with 409 session-live (carrying forceable: true), and the UI asks for a second confirmation; on confirmation the files are overwritten with zeros and only then unlinked, so the bytes are really gone even though the writer still holds a descriptor;
  • one of its subagent sessions is still live → 409 descendant-live, same forced confirmation.

The cost: such a conversation leaves the session list only after the Harness restarts, and if it is still open in a tab and you keep sending messages it may create a fresh (empty) log. The second confirmation says exactly that.

2. Nothing is deleted unless its absence is proven safe. Attachment objects are content-addressed and may be shared between sessions, so an id is dropped only after every surviving session's log has been scanned and none of them mentions it. The scan streams and decompresses without materializing events and is bounded by a session ceiling and a decompressed-byte budget; when either is exceeded, or a log cannot be read, the bytes are kept and a warning is reported.

Other engineering constraints:

  • Session ids are escaped with an encodeSegment byte-for-byte identical to the JSONL backend's before touching a path, so ../, absolute paths, and NUL cannot escape the roots;
  • The package imports only node: builtins and no private @deepseek-ai/* package, so it publishes as an ordinary npm package;
  • Both HTTP routes pass the composition's connection.requestRejection trust fence first (Host/Origin checks plus the browser session cookie); unauthenticated requests get 401/403.

Install

pnpm is assumed (DSH profiles are pnpm-managed).

From npm, once published:

dsh plugin install @fufu1437/dsh-delete-chat

From a local checkout:

pnpm add @fufu1437/dsh-delete-chat     # or as a local link dependency
dsh plugin install /absolute/path/to/dsh-delete-chat

The in-Harness plugin manager works too: call install_bundle with the package directory, a .tgz, or the npm package name.

The Host half requires a Harness restart to take effect after an update: in this profile the hmr row is configured with root: [] (no plugin module roots watched), so the process caches the loaded module generation. The Client half takes effect as the page loads.

Usage

  1. Hover a conversation row in the left sidebar and open its … menu;
  2. choose "Delete conversation…";
  3. the dialog shows the title, the irreversibility warning, and Cancel / Delete permanently — nothing is measured or listed first;
  4. press "Delete permanently": the dialog closes, the deletion continues in the background, and the row disappears when the Host broadcasts the removal;
  5. if the conversation is still open in this Harness process, a second confirmation appears ("Delete anyway") explaining that the files are overwritten in place and that it leaves the list only after a restart;
  6. any other failure (a response currently generating) raises a dismissible alert card in the corner with the reason.

Configuration

Override in the profile's cordis.patch.yml:

- id: fufu-delete-chat
  name: '@fufu1437/dsh-delete-chat'
  config:
    dshHome: /home/me/.dsh
    deleteAttachments: true
    deleteDescendants: true
    scanLimit: 500
    scanByteLimit: 2147483648
FieldDefaultMeaning
dshHome$DSH_HOME → ~/.dshHarness home; every other root derives from it
sessionsRoot<dshHome>/sessionsSession log root
storagesRoot<dshHome>/storagesstorage-json root
attachmentsRoot<dshHome>/attachments/v1Attachment root
llmFilesRoot<dshHome>/llm-deepseekDeepSeek upload-cache root
spillRootsdiscovered $TMPDIR/dsh-spill-*Spill roots to sweep
deleteAttachmentstrueRun the attachment proof and erase unreferenced uploads
deleteDescendantstrueAlso delete durable subagent sessions
scanLimit500Maximum session logs the attachment proof reads
scanByteLimit2 GiBMaximum decompressed bytes the proof reads

Verification

pnpm test          # fixture self-test: 68 assertions, touches no real data
pnpm run check     # syntax-check both halves

scripts/selftest.mjs builds a complete throwaway harness home (session logs, projection cache, workspace index, legacy feedback sidecar, spill files, attachments, upload cache), drives the plan/execute pair with a scripted Host context, and asserts what survived:

  • every artifact class is erased while another session's data stays intact;
  • live, running, and live-descendant conversations are refused;
  • an attachment referenced by another session is kept; an exclusively referenced one is erased;
  • a truncated scan or an undecompressable log keeps the bytes and warns;
  • a hostile session id cannot escape the roots;
  • the in-place overwrite really happens: the test keeps an open descriptor on the log (standing in for the live writer) and reads zeros through it after the deletion, proving the content was erased rather than merely unlinked.

.tmp/e2e.mjs is an end-to-end script against the running Host (never published): it mints the same browser-session cookie the page uses, creates a synthetic session in the real DSH home (log + projection cache + spill + attachment), calls delete through the real trust fence, asserts the artifacts are gone, and verifies that a live conversation is refused with 409 and that the removed /inspect route is gone. It needs write access and cleans up after itself.

.tmp/proof-scan.mjs runs the full attachment proof read-only over the real corpus (187 session logs on this machine complete in about 1.5 s).

Known limitations

  • A conversation that is generating a response cannot be deleted until it stops; that is the one hard refusal, and it is deliberate.
  • A conversation that was merely opened can be deleted, but its live session object survives until the Harness restarts (DSH has no public session-release API): the bytes are overwritten and unlinked, yet the session can still appear in the list or create a fresh empty log if you keep sending messages in its open tab.
  • Telemetry already exported cannot be recalled: session-telemetry-otel may already have shipped a log prefix to a remote collector in FEEDBACK_ONLY mode; this plugin can only erase local data.
  • Derived-cache boundary: removing a request-image cache entry requires the matching record in files-v3.json; other derived copies that leave no attachment reference in the log are out of scope.
  • The legacy message_feedback.json sidecar is not read by the current service; this plugin reads and rewrites it directly.
  • workspace.json is edited through the workspace registry API when that service exists, and directly only when it does not (so nothing can overwrite the change from memory).
  • The default local spill root is a temp directory; if a Harness once used a different root that has since been cleaned up, there is nothing left to delete.

Publishing

pnpm run check && pnpm test
pnpm publish --access public     # scoped packages need public access

prepublishOnly runs the syntax check and the self-test first.

License

MIT

Related plugins