deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
Lzcdebear/better-dsh-session-deletetool
Delete DSH conversations instead of archiving them, see the whole family a conversation spawned (its subagents and derived conversations), and delete in bulk. / 真正删除 DeepSeek Harness 里的会话,并看清它生出的子智能体与派生对话。
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:Lzcdebear/better-dsh-session-deletetool
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
Delete a conversation from DeepSeek Harness — DSH ships archive only.
DSH can only archive a conversation. Archiving hides the row from the sidebar and keeps every
artifact: the log, the workspace account, the projection checkpoint all stay on disk, and the
conversation can be restored at any time. The persistence seam has no deletion API at all —
@deepseek-ai/dsh-session-persistence-jsonl states it plainly:
Nothing deletes session files — logs accumulate under
rootuntil removed externally; the seam has no deletion API.
So a long-lived profile accumulates conversations forever. This plugin adds the missing step: a
Delete conversation row in each session's ⋯ menu that really removes the data — and the dialog
it opens is also the one place that shows a conversation's whole family before you touch it.
For the chosen session, the Host half:
ctx.sessionPersistence.locate(header) gives the
path);SessionHeader.parentSession), and writes
that field for every relation it creates: the Subagent runtime sets it with origin: 'subagent',
a fork sets it with isSeeded, and Agent Teams resolves its roster through the same field. The
plugin walks that lineage breadth-first and unions it with the durable subagentCatalog
projection (ctx.subagents.listDescendants), so a child whose own header no longer reads is still
named. Descendants are deleted deepest first, gathered before anything is removed, and only
the ticked ones go — up to 200 per request;sessionIds
(Workspace.detachSession) and the registry-global archive and pin sets;session_projcache domain record and its <id>.json
file;ctx.terminals are part of no admission family, and the
service only reaps them when the Agent is released, which can be long after the log is gone;api-session/removed per removed id, the same event the shipped
Session controller emits when a Session is disposed, so the rows leave the sidebar immediately.The sidebar lists a subagent session and a derived conversation as rows of their own, and nothing there says which conversation spawned them; DSH itself never draws that relation. This dialog is the one place it is drawn: the session's whole family as one tree, the branches below the branches included.
A row's checkbox covers its own subtree: ticked when everything below it is ticked, mixed when only part of it is, and pressing it takes or clears that whole subtree. A block's checkbox covers the rows the block lists. The delete set is one set of ids, with no second "excluded rows" state beside it, so the picture and the footer's count cannot drift apart.
Leads that could not be read are reported above the list (a branch whose directory would not open, for example) instead of quietly costing rows: a missing row and a conversation that genuinely has no children used to look exactly alike.
Confirming posts exactly the ticked ids and the button states how many conversations will go. Unticked descendants survive as conversation roots. The selection is validated against the Host's own walk, so a request can only ever name Sessions in that lineage. A fork is a conversation in its own right, which is exactly why it is listed with a checkbox instead of being taken silently.
A trash control sits in the workspace section header, immediately left of the search icon (it
draws the deleting icon.svg glyph). It opens one dialog over every conversation the Host knows:
Project_lzc), else the id. A conversation that was
never renamed therefore reads as its directory rather than as untitled.The dialog carries the same stop unfinished work first switch as the single-session one, on by default. Confirming runs the delete above once per ticked root; a root that fails is listed and the rest still go.
Running work blocks it — being open does not. The gate is DSH's own archive admission, the
workspace/session-activity waterfall: the Agent registry reports a running turn, the job registry
reports background jobs, the Subagent runtime reports running descendants, Schedule reports active
reminders. The question is asked once per Session in the delete set, because the admission answers
per Session: a descendant's running turn or background job is not reported when only the target is
asked. The dialog names what is still running — on each row and in the summary — and offers
Stop and delete, which dispatches workspace/session-stop for every busy Session first, exactly
what archiveSession(id, { stopActivity: true }) does.
A session the Host still holds open (ctx.sessions / ctx.agents) is deleted anyway. Measured on
Windows: rm succeeds while the append handle is open, the directory entry disappears at once, and
later appends land in the unlinked file instead of resurrecting it. An earlier version refused such
sessions with "switch away first", which wrongly blocked conversations that were not on screen.
~/.dsh/attachments/v1/objects/<hash>, a content-addressed store shared by sessions, and the
service has no reference counting, so those blobs stay.dsh-session-query-sqlite is a derived index that
reconciles against persistence on every search, so a deleted source stops being returned from the
next search on.Everything below goes through DSH's own plugin manager, which accepts an install spec
(@deepseek-ai/dsh-plugin-manager): a registry name, a git host shorthand, a repository URL, a
tarball, or an absolute local path. The npm route also works with a plain npm install, into the
profile directory. Pick whichever route your network allows.
better-dsh-session-deletetool)The registry route, and the only one that does not need github.com:
npm install better-dsh-session-deletetool
DSH's own plugin manager takes the registry name directly, which is the better route inside the app: it records the dependency in the profile and reloads it.
better-dsh-session-deletetool.plugin_manager { action: "install_bundle", target: "better-dsh-session-deletetool" }.Pin a version the way npm writes it: better-dsh-session-deletetool@0.4.0.
Spec:
github:Lzcdebear/better-dsh-session-deletetool
or, equivalently:
https://github.com/Lzcdebear/better-dsh-session-deletetool
Give it to DSH:
plugin_manager tool take exactly the same spec string.)plugin_manager with
action: "install_bundle" and target: "github:Lzcdebear/better-dsh-session-deletetool".Pin a ref with #: github:Lzcdebear/better-dsh-session-deletetool#v0.1.0.
If the connection check fails, DSH reports a bounded log path. On a network where github.com is not
reachable, use route 1, 3 or 4 — or point git at your proxy first
(git config --global http.proxy http://127.0.0.1:7890).
Download the repository (ZIP or git clone), unpack it anywhere, then install the absolute
directory:
plugin_manager { action: "install_bundle", target: "D:\\plugins\\better-dsh-session-deletetool" }
DSH records a link: dependency and reloads the profile. This is the route used to develop the
plugin, and the one to use behind a restrictive network.
https://github.com/Lzcdebear/better-dsh-session-deletetool/archive/refs/heads/main.tar.gz
Same install entry as route 2; useful when git is unavailable but HTTPS is not.
The Host half loads with the profile. The Client half appears after the page is refreshed. The row
shows up in the session ⋯ menu as 删除会话 / Delete conversation.
To remove the plugin again, use the same manager:
plugin_manager { action: "remove_bundle", target: "better-dsh-session-deletetool" } (the bundle key is the
package name).
⋯ menu on any session row and pick Delete conversation.For bulk: press the trash control in the workspace section header, left of the search icon, tick conversations in the Workspace-sectioned list (ticking a parent brings its children, a child can be ticked or unticked on its own), then confirm.
The Client half reaches the Host over four same-origin exact routes on ctx.webServer. A
build-free plain-JavaScript bundle cannot declare a typed ctx.remote namespace (that needs
generated Typert descriptors), so this uses the same transport the community plugin dshmarket
uses.
| Route | Method | Purpose |
|---|---|---|
/better-dsh-session-deletetool/inspect?sessionId=… |
GET | stored / open / agent / running / activity / artifactDirectory / warnings (the leads that could not be read, so a missing branch is never mistaken for a childless one), and descendants = { count, subagents, derived, truncated, maxDeletable, items[] } where each item carries id, kind, depth, parentId, title, open, agent, running and its own activity |
/better-dsh-session-deletetool/delete |
POST | body { sessionId, stop?, descendants? } — descendants omitted means the whole family, an empty array means the session alone; returns removed, descendants (each with its kind), kept, stoppedActivity, terminalsKilled, warnings, runtime, activity. A name outside the family is refused with 400 unknown-descendant before anything is removed |
/better-dsh-session-deletetool/catalog |
GET | { ok, workspaces: [{ key, workspaceId, title, path, sessions[] }], totals }; each session row carries id, kind (root / subagent / derived), depth, parentId, hasChildren, family, subagents, derived, title, cwd, open, agent, running, activity. Section order is the registry's own Workspace order, and the section whose workspaceId is null is Ungrouped |
/better-dsh-session-deletetool/delete-batch |
POST | body { roots: [{ sessionId, descendants? }], stop? }, each root running the single-session delete once; returns { ok, roots, removed[], failed[] }. A failing root is reported as one failed entry and the rest are still attempted. At most 200 roots per request |
All four routes carry their own same-origin gate: Host must be loopback, sec-fetch-site must not
be cross-site, and a present Origin must match Host. Another site's page cannot reach them.
| File | Role |
|---|---|
host.js |
Host half: the four routes, artifact/accounting/cache removal, the subagent subtree, and the Workspace-sectioned catalog |
client.js |
Client half: the sidebar.workspaces.session.menu.item row (order 500), the bulk control, and the two shell.overlay dialogs |
cordis.patch.yml |
Inserts the Host row into the profile's layer stack |
test/host.test.mjs |
Route tests over real temporary directories |
icon.svg |
Plugin artwork (the bulk control draws deleting icon.svg) |
Styles use only host theme tokens (--dsw-alias-*), so light and dark both read; the UI is
localized (English / 简体中文) through the Client locale service.
Why the bulk entry is not a slot registration of its own: sidebar.workspaces is a single slot, so
a second registrant would shadow the shipped session browser instead of sitting beside it. The
Client half therefore registers in sidebar.footer.action (rendering nothing there) and mounts its
control into the browser's own search cell through a portal and one MutationObserver, which
re-creates the control when React re-renders that header. That insertion carries no authority beyond
opening the dialog; the two Host routes do the work. If a future harness renames that cell's CSS
class, the control stops appearing and nothing else changes.
node --test test/host.test.mjs
The tests drive the real apply() registration against fake Host services and real temporary
directories: deletion, the running-work gate and its stop path, the subagent subtree, the inspect
report, and the refusal paths (bad id, bad body, wrong method, cross-site origin).
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。