better-dsh-session-deletetool 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,299 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ **The package is `better-dsh-session-deletetool` now.** Every name that pointed at the old identity was
6
+ renamed in one pass, so nothing resolves the old spelling any more:
7
+
8
+ - the npm package name, the GitHub repository (`Lzcdebear/better-dsh-session-deletetool`), and the
9
+ `repository` / `homepage` / `bugs` URLs plus every install target in the READMEs;
10
+ - the four Host routes: `/better-dsh-session-deletetool/inspect`, `/delete`, `/catalog` and
11
+ `/delete-batch`;
12
+ - the client module id, the locale namespace, the sidebar menu row, both `shell.overlay` seats and the
13
+ bulk anchor's registration ids, the `data-dsh-plugin` marker, and the React display name;
14
+ - the Host loader row in `cordis.patch.yml` — both the row `id` and the module `name` the profile
15
+ imports;
16
+ - the Host's `[better-dsh-session-deletetool]` log prefix and its effect labels.
17
+
18
+ **The family view is written down now.** The dialogs have drawn a conversation's subagents and derived
19
+ conversations as one tree for several releases, but neither README said so out loud. Both now open that
20
+ section by saying that the sidebar cannot show the relation and the dialog is the one place it is
21
+ drawn, and the npm description says it too.
22
+
23
+ A profile that had the old name linked has to link the new one: the bundle key in
24
+ `dsh.profile.bundles` and the dependency key in the profile's own `package.json` both change.
25
+
26
+ ## 0.3.9
27
+
28
+ **Both dialogs now draw the same family tree.** The single conversation's dialog and the bulk one share
29
+ one renderer: a row per Session, and under every row a collapsible **子智能体 (n)** block followed by
30
+ a collapsible **派生对话 (n)** block holding that row's own direct children. A derived conversation
31
+ keeps its own subagents *and* its own derived conversations inside its own block, one step further in,
32
+ so a fork of a fork is nested where it belongs instead of appearing in a second list that only shares
33
+ a heading with its parent.
34
+
35
+ **The duplicated rows are gone.** 0.3.7 drew each family twice over, once per relation, each pass
36
+ walking the whole forest: a subagent hanging under a fork was listed both flat in the root's 子智能体
37
+ branch and again inside its real parent. One forest is now built once (`buildForest`) and drawn once
38
+ (`FamilyTree`), so every row is drawn by exactly one parent — which is what makes the shape readable.
39
+
40
+ **Every row's checkbox means its own subtree.** A row is ticked when everything below it is ticked,
41
+ mixed when only part of it is, and pressing it takes or clears that whole subtree; a block's checkbox
42
+ covers the rows the block lists. The old "selected roots plus excluded children" pair is gone: the
43
+ delete set is one set of ids, so the picture and the count can no longer drift apart.
44
+
45
+ **In the bulk dialog a child can now be ticked on its own.** Every ticked row whose parent is not
46
+ ticked is sent as a delete root of its own, carrying the ticked rows below it — so ticking a single
47
+ subagent deletes just that subagent, and ticking a parent takes its selected family with it.
48
+
49
+ **Each row carries its kind's glyph**: a document for a conversation, a robot head for the 子智能体
50
+ block and a person for a subagent, a chain for the 派生对话 block and a chat bubble for a derived
51
+ conversation — 16px outline paths, `fill: none`, `stroke: currentColor`, matching the icon set they
52
+ sit beside. Nothing else changed: the nesting is still the indentation alone, one step per level, with
53
+ no connectors and no guide lines.
54
+
55
+ ## 0.3.7
56
+
57
+ **The connector lines are gone.** The tree's nesting is the indentation alone: one step per level, the
58
+ same reading the rest of the sidebar uses. A drawn guide line under every parent added nothing the
59
+ indent did not already say, and it made the rows busier than the list they sit in.
60
+
61
+ Everything else from 0.3.6 stands: row 1 is this conversation, row 2 is its own subagent branch, row 3
62
+ is the forked conversations with each fork's subagents inside it and a fork of a fork nested in its
63
+ parent's block.
64
+
65
+ ## 0.3.6
66
+
67
+ **The picker is the family tree now, drawn as one.** 0.2.1's two flat lists could not say what the
68
+ reference layout says, so the dialog is drawn that way from the top down:
69
+
70
+ - row 1 is this conversation, carrying the whole family's checkbox and named as the sidebar names it;
71
+ - row 2 is one collapsible **子智能体 (n)** branch holding this conversation's own subagents;
72
+ - row 3 is one collapsible **派生对话 (n)** branch, and each fork inside it keeps *its own* subagent
73
+ branch — so a fork of a fork is drawn inside its parent's block, indented one more step, instead of
74
+ being listed beside it. A subagent that spawned a conversation keeps that conversation inside the
75
+ subagent's own block for the same reason.
76
+ - Guide lines drop from each parent to the children it draws, and indent tracks the tree, so a level-3
77
+ row reads as hanging off the level-2 row above it and not merely as "somewhere deeper".
78
+
79
+ **The layout came from the sketch, not the data model.** The Host still answers one deepest-first
80
+ list; the client arranges it into a tree and draws each branch as a *view* of that tree, indented from
81
+ the nearest ancestor the branch actually draws. Nothing is ever indented under a row that is not on
82
+ screen.
83
+
84
+ **Ticks are derived, not stored twice.** A row is in the delete set when it is a selected root or sits
85
+ beneath one. A subagent is a Session of its own, so a fork's checkbox reports the subagents under it
86
+ without claiming them for a second delete — which is what kept the footer's count honest.
87
+
88
+ ## 0.3.5
89
+
90
+ **A group now shows its own nesting.** 0.3.4 arranged the family as one tree but then filtered it into
91
+ the two groups, and filtering flattened the tree: a level-2 child stayed indented under a level-1
92
+ parent the group did not draw, so nothing on screen said which level-1 child it belonged to.
93
+ `groupNodes` walks the arranged family in pre-order and counts each group's indent from the nearest
94
+ ancestor *that group draws*, so a parent is followed by its own children, one step in, and the next
95
+ family starts at the top of the column again. A group's checkbox, its root rows and the delete payload
96
+ are unchanged: a picked row still answers to the same root it did before.
97
+
98
+ **The family's read failures are reported, not swallowed.** The descendant list comes from two durable
99
+ sources, and either one failing used to be invisible — the dialog simply drew fewer rows, which is
100
+ indistinguishable from a conversation that genuinely has no children. `/inspect` now carries the
101
+ warnings it collected (a catalog that would not open, a branch that could not be read), and the
102
+ per-Session dialog prints them above the list.
103
+
104
+ ## 0.3.4
105
+
106
+ **The per-Session picker is 0.2.1's layout again, with the lineage fixed.** 0.3.2 removed the two
107
+ collapsible groups to fix a nesting bug; that threw away a shape that was doing its job, so the groups
108
+ are back: one master row, then *subagent conversations* and *forked conversations*, each with its own
109
+ count, its own checkbox and its own disclosure caret, and every row keeps its title, its id tail and
110
+ its state badges.
111
+
112
+ **What actually changed is where a row's position comes from.** Both 0.2.1 and 0.3.2 drew rows in the
113
+ order the Host sent them, and the Host sends the family deepest-first because that is the order its
114
+ own delete walk wants. Splitting that into groups put a grandchild in one group and its parent in
115
+ another, where the grandchild became a top-level row and drew indented above the row it hangs under.
116
+ The whole family is now arranged once — every row follows the row it hangs under, with the depth
117
+ counted from the nearest row the picker actually draws — and only then split by relation, so each
118
+ group keeps its own order without ever inverting a parent and its child.
119
+
120
+ **Indentation is one step per level.** Rows are indented by the arranged depth alone (16px a level, the
121
+ same step the bulk dialog uses), where 0.2.1 added a fixed extra level for every row: a
122
+ one-level-deep child read as though it hung off something that was not there.
123
+
124
+ ## 0.3.3
125
+
126
+ **Missing subagents in the per-Session dialog, fixed at the source.** The family was read from two
127
+ places, and the subagent half leaned on `ctx.subagents.listDescendants`, which walks that service's
128
+ own session store: when that lookup is unavailable, the call throws and *every* subagent disappears
129
+ from the dialog at once, leaving only the forked conversations the header lineage names. The catalog
130
+ is now read the way the Subagent runtime itself reads it — per parent, from that parent's own
131
+ `subagentCatalog` projection through `sessionQuery.observeSession` — and walked from the target
132
+ downwards. The service call is still made, merged by closest depth, so neither source is a single
133
+ point of failure. Observation leases are freed through `Symbol.dispose` in a `finally`: the lease has
134
+ no `release()`, and a walk that never disposed would pin every parent it read in the query cache.
135
+
136
+ **Nesting now comes from the right parent.** Because the walk descends through each parent's own
137
+ catalog, a subagent a *child conversation* spawned is listed under that child rather than under the
138
+ target, and the header lineage now descends through children the catalog did not produce too. So the
139
+ dialog draws one family per level:
140
+
141
+ ```
142
+ 母会话
143
+ 子代理
144
+ 子会话
145
+ 子会话 1 的子代理
146
+ 子会话 2
147
+ 子会话 2 的子代理
148
+ ```
149
+
150
+ ## 0.3.2
151
+
152
+ **The 子 / 母 label is gone.** The nesting reads from indentation alone, as it does in the rest of the
153
+ sidebar. A family shows its shape: one step of indent per level, parents above their own children.
154
+ The catalog still reports `hasChildren` and the family counts, so nothing else had to change.
155
+
156
+ **The per-Session picker draws the family as one tree.** It used to split the descendants into two
157
+ collapsible groups by relation — *subagent conversations* and *forked conversations* — and arrange
158
+ each group on its own. A grandchild whose parent landed in the other group was therefore drawn as a
159
+ top-level row, indented, above the row it actually hangs under. The family is now arranged as a whole
160
+ with the same rule the bulk view uses, so every row follows the row it hangs under and the
161
+ indentation is the nesting. The per-kind groups and their collapse controls went with it: the list is
162
+ one tree, and the master checkbox still clears or takes the whole family.
163
+
164
+ ## 0.3.1
165
+
166
+ Four fixes to the bulk view, three of them visible on first use.
167
+
168
+ **The glyph's colour now matches its neighbours.** The control was drawn as the source SVG is
169
+ authored — a filled glyph — while the shipped product icon set is outline-only (`fill: none`,
170
+ `stroke: currentColor`, `strokeWidth: 1` on a 16px box). A filled path painting a 32-unit outline
171
+ shape rendered as a blob at 16px, so the button looked empty beside the search icon. The glyph is now
172
+ stroked in the icon set's own convention, with the stroke scaled for the halved viewBox.
173
+
174
+ **Every row carries its 子 / 母 mark.** A conversation with no children of its own in the list left
175
+ the mark blank. A row that is not a child now reads as 母 by default, so the three states are 母,
176
+ 子, and 子母 for a child that has children of its own. The mark's cell is sized for two characters, so
177
+ 子母 no longer shifts the title along.
178
+
179
+ **Names come from the Session, not from "untitled".** The title read was wrong: the folded snapshot
180
+ is `{ session, title: { title, … } }`, and the code read `value.title` — an object, which failed its
181
+ own string check, so every Session reported no title and both dialogs fell back to *untitled*. Rows
182
+ now resolve a display name the way the sidebar does: the durable title when the log carries one, else
183
+ the final segment of the project directory, else the id. That fixes the bulk list and the 0.2.1
184
+ per-Session dialog together, the latter including its subagent and forked children, which now read as
185
+ the project directory rather than *untitled* when they were never renamed.
186
+
187
+ **Lineage children can carry their own directory.** The descendant walk now passes each child's
188
+ `cwd` from its header, so a descendant that was never named falls back to its project directory
189
+ before falling back to its id.
190
+
191
+ ## 0.3.0
192
+
193
+ One control beside the workspace search icon deletes many conversations at once.
194
+
195
+ **A bulk entry beside the search icon.** The sidebar's workspace header gains a trash control, drawn
196
+ from `deleting icon.svg`, immediately left of the search icon. It opens one dialog for every
197
+ conversation the Host knows.
198
+
199
+ **The list is grouped by Workspace and keeps lineages together.** Rows are sectioned by the Workspace
200
+ that owns the conversation's directory, in the registry's own order, with one "未归类" section for
201
+ conversations no Workspace owns. Inside a section, parents lead their children, indented by lineage
202
+ depth. Every row leads with the blue **子** / **母** mark: `母` for a conversation with children in
203
+ this list, `子` for one spawned as a subagent or forked off another, `子母` for both.
204
+
205
+ **Selection follows the family, per row.** Ticking a parent takes its whole family with it; ticking a
206
+ child selects it on its own. A child can be taken back off a ticked family without untick-ing the
207
+ parent, and the footer always states how many conversations the press will delete. The dialog carries
208
+ the same "stop unfinished work first" choice the single-Session dialog offers.
209
+
210
+ **API.** `GET /catalog` answers
211
+ `{ ok, workspaces: [{ key, workspaceId, title, path, sessions[] }], totals }`, and each session row
212
+ carries `id`, `kind` (`root` / `subagent` / `derived`), `depth`, `parentId`, `hasChildren`, `family`,
213
+ `subagents`, `derived`, `title`, `cwd`, `open`, `agent`, `running` and `activity`.
214
+ `POST /delete-batch` takes `{ roots: [{ sessionId, descendants? }], stop? }` and answers
215
+ `{ ok, roots, removed[], failed[] }` — one entry per root, so a failing root is reported without
216
+ aborting the rest.
217
+
218
+ **Where the control is mounted, and why.** `sidebar.workspaces` is a `single` slot, so a second
219
+ registrant would shadow the shipped browser instead of sitting beside it. The plugin therefore
220
+ registers in `sidebar.footer.action` (which renders nothing itself) and mounts its control into the
221
+ browsing region's own search slot through a portal and one `MutationObserver`, re-creating the
222
+ control whenever React re-renders that header. The insertion carries no authority: it only opens the
223
+ dialog, and the two Host routes above do all the work. If a future harness renames that slot's CSS
224
+ class, the control stops appearing and nothing else changes.
225
+
226
+ **Unchanged.** The per-Session "⋯" menu entry and its descendant picker behave exactly as in 0.2.1;
227
+ the bulk path calls the same delete for each selected conversation.
228
+
229
+ ## 0.2.1
230
+
231
+ Deleting a conversation is a choice now, not a take-it-all.
232
+
233
+ **Pick what goes with it.** The confirmation dialog lists the session's whole family and lets you
234
+ choose:
235
+
236
+ - one **delete all** checkbox, with an indeterminate state when the selection is partial;
237
+ - one collapsible group per relation — *subagent conversations* and *forked conversations*;
238
+ - one checkbox per descendant, indented by lineage depth, labelled with its title and the tail of its
239
+ id, and badged when it is open or still has work running.
240
+
241
+ Confirming posts exactly the ticked ids, and the button states how many conversations will go.
242
+ Unticked descendants survive as conversation roots.
243
+
244
+ **The family is complete and the selection is validated.** Descendants come from the header lineage
245
+ every Session carries (`SessionHeader.parentSession`) — which covers subagent sessions, forked
246
+ conversations, and the child Sessions Agent Teams provisions — unioned with the durable
247
+ `subagentCatalog` projection for children whose own header no longer reads. The Host validates the
248
+ client's selection against its own walk, so a request can only ever name Sessions in that lineage
249
+ (`400 unknown-descendant` otherwise). Omitting the field still means "all of them".
250
+
251
+ **A gate bug, fixed.** Running work was only checked for the target Session, but DSH's archive
252
+ admission answers per Session: a descendant's running turn or background job was invisible, so a child
253
+ could be deleted mid-run. Every Session in the delete set is now asked, the refusal names each busy
254
+ Session (`activeSessions` in the details, badges on the rows), and `stop: true` dispatches
255
+ `workspace/session-stop` for all of them.
256
+
257
+ **Shells no longer outlive their conversation.** Terminals belong to no admission family, and the
258
+ service only reaps them when the Agent is released — which can be long after the log is gone. Each
259
+ deleted Session's terminals are now closed, reported as `terminalsKilled`.
260
+
261
+ **API.** `GET /inspect` answers with `descendants: { count, subagents, derived, truncated,
262
+ maxDeletable, items[] }`, each item carrying `id`, `kind`, `depth`, `parentId`, `title`, `open`,
263
+ `agent`, `running` and its own `activity`. `POST /delete` takes `{ sessionId, stop?, descendants? }`
264
+ and answers with `kept`, `terminalsKilled` and `warnings` beside the per-Session reports. The
265
+ request-body ceiling is 64 KiB so a few hundred ids fit, and the 200-per-request cap applies to the
266
+ selection rather than to the whole family.
267
+
268
+ **Audited, deliberately unchanged.** Attachment blobs stay (content-addressed and shared; the service
269
+ has no reference counting), and the derived search index keeps reconciling itself.
270
+
271
+ ## 0.2.0
272
+
273
+ - **Forked conversations are deleted with their source.** A descendant is now read from the header
274
+ lineage every Session carries (`SessionHeader.parentSession`), which DSH writes for both
275
+ relations: the Subagent runtime sets it with `origin: 'subagent'`, and a fork sets it with
276
+ `isSeeded`. 0.1.0 walked only the subagent catalog, so a conversation forked off the deleted one
277
+ survived.
278
+ - The confirmation dialog names what goes with the delete by kind — *N subagent conversations* and
279
+ *N conversations forked off it* — so deleting a family is never silent.
280
+ - `/inspect` reports `descendants: { count, subagents, derived, ids, capped }`.
281
+ - Each descendant report in the delete response carries its `kind` (`subagent` / `derived`).
282
+ - The lineage walk is breadth-first with a visited set and a depth cap: DSH's own lineage traversal
283
+ has no guard against a hand-edited `parentSession` cycle.
284
+
285
+ ## 0.1.0
286
+
287
+ First release.
288
+
289
+ - Adds a **Delete conversation** row (order 500) to every session's `⋯` menu, with a confirmation
290
+ dialog that states the session's real state before committing.
291
+ - Removes the session's artifact directory (every retained format generation), its workspace
292
+ account, its archive/pin membership, and its projection-cache record, then emits
293
+ `api-session/removed` so connected pages drop the row at once.
294
+ - Removes subagent descendants too, deepest first, collected before anything is deleted and capped
295
+ at 200 sessions per delete.
296
+ - Gates on running work only (DSH's own `workspace/session-activity` admission), with a
297
+ **Stop and delete** path that dispatches `workspace/session-stop` first.
298
+ - Deletes sessions the Host still holds open, instead of refusing them: measured on Windows, `rm`
299
+ succeeds with the append handle open and later appends do not resurrect the file.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 陆知辰 (Lzcdebear)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # better-dsh-session-deletetool
2
+
3
+ **Delete a conversation from DeepSeek Harness — DSH ships archive only.**
4
+
5
+ [中文说明](README.zh-CN.md)
6
+
7
+ ---
8
+
9
+ ## Why this exists
10
+
11
+ DSH can only *archive* a conversation. Archiving hides the row from the sidebar and keeps every
12
+ artifact: the log, the workspace account, the projection checkpoint all stay on disk, and the
13
+ conversation can be restored at any time. The persistence seam has no deletion API at all —
14
+ `@deepseek-ai/dsh-session-persistence-jsonl` states it plainly:
15
+
16
+ > **Nothing deletes session files** — logs accumulate under `root` until removed externally; the
17
+ > seam has no deletion API.
18
+
19
+ So a long-lived profile accumulates conversations forever. This plugin adds the missing step: a
20
+ **Delete conversation** row in each session's `⋯` menu that really removes the data — and the dialog
21
+ it opens is also the one place that shows a conversation's whole family before you touch it.
22
+
23
+ ## What it deletes
24
+
25
+ For the chosen session, the Host half:
26
+
27
+ 1. **removes the session's artifact directory** — the current log, every retained historical format
28
+ generation, and any session-local file in it (`ctx.sessionPersistence.locate(header)` gives the
29
+ path);
30
+ 2. **removes the descendants you selected** — child sessions are sessions of their own with their own
31
+ logs. DSH records every child in its parent's header (`SessionHeader.parentSession`), and writes
32
+ that field for every relation it creates: the Subagent runtime sets it with `origin: 'subagent'`,
33
+ a fork sets it with `isSeeded`, and Agent Teams resolves its roster through the same field. The
34
+ plugin walks that lineage breadth-first and unions it with the durable `subagentCatalog`
35
+ projection (`ctx.subagents.listDescendants`), so a child whose own header no longer reads is still
36
+ named. Descendants are deleted **deepest first**, gathered *before* anything is removed, and only
37
+ the ticked ones go — up to 200 per request;
38
+ 3. **drops the workspace account** — the id leaves every Workspace record's `sessionIds`
39
+ (`Workspace.detachSession`) and the registry-global archive and pin sets;
40
+ 4. **drops the projection checkpoint** — the `session_projcache` domain record and its `<id>.json`
41
+ file;
42
+ 5. **closes the shells the session owned** — `ctx.terminals` are part of no admission family, and the
43
+ service only reaps them when the Agent is released, which can be long after the log is gone;
44
+ 6. **tells connected pages** — one `api-session/removed` per removed id, the same event the shipped
45
+ Session controller emits when a Session is disposed, so the rows leave the sidebar immediately.
46
+
47
+ ### Seeing the family, and choosing what goes with it
48
+
49
+ The sidebar lists a subagent session and a derived conversation as rows of their own, and nothing there
50
+ says which conversation spawned them; DSH itself never draws that relation. This dialog is the one
51
+ place it is drawn: the session's whole family as **one tree**, the branches below the branches
52
+ included.
53
+
54
+ - row 1 is this conversation, carrying the whole family's checkbox and named as the sidebar names it;
55
+ - row 2 is one collapsible **Subagents (n)** branch holding the subagents this conversation spawned;
56
+ - row 3 is one collapsible **Forked conversations (n)** branch, and each fork inside it keeps *its own*
57
+ Subagents branch — so a fork of a fork is drawn inside its parent's block, one step further in,
58
+ instead of being listed beside it. A subagent that spawned a conversation keeps that conversation
59
+ inside the subagent's own block for the same reason;
60
+ - every row has a checkbox, its name and the tail of its id, and is badged when it is open or has
61
+ unfinished work. Names follow the sidebar's own rule — the durable title, else the final segment of
62
+ the project directory, else the id — so nothing reads as *untitled*;
63
+ - the nesting is the **indentation** alone, one step per level: a parent is followed immediately by the
64
+ children it draws, so a level-3 row reads as hanging off the level-2 row above it rather than merely
65
+ as "somewhere deeper".
66
+
67
+ A row's checkbox covers **its own subtree**: ticked when everything below it is ticked, mixed when only
68
+ part of it is, and pressing it takes or clears that whole subtree. A block's checkbox covers the rows
69
+ the block lists. The delete set is one set of ids, with no second "excluded rows" state beside it, so
70
+ the picture and the footer's count cannot drift apart.
71
+
72
+ Leads that could not be read are reported above the list (a branch whose directory would not open, for
73
+ example) instead of quietly costing rows: a missing row and a conversation that genuinely has no
74
+ children used to look exactly alike.
75
+
76
+ Confirming posts exactly the ticked ids and the button states how many conversations will go.
77
+ Unticked descendants survive as conversation roots. The selection is validated against the Host's own
78
+ walk, so a request can only ever name Sessions in that lineage. A fork is a conversation in its own
79
+ right, which is exactly why it is listed with a checkbox instead of being taken silently.
80
+
81
+ ### Deleting in bulk
82
+
83
+ A trash control sits in the workspace section header, immediately **left of the search icon** (it
84
+ draws the `deleting icon.svg` glyph). It opens one dialog over every conversation the Host knows:
85
+
86
+ - **Sectioned by Workspace**, in the registry's own order, each headed by the Workspace title and its
87
+ conversation count; conversations no Workspace owns land in a final **Ungrouped** section.
88
+ - **Each conversation is drawn as the same tree the single-session dialog draws**: under a row come its
89
+ collapsible **Subagents (n)** and **Forked conversations (n)** blocks, and a derived conversation
90
+ carries its own level inside its own block, so the hierarchy is the indentation alone.
91
+ - **Names follow the sidebar's own rule**: the durable title when the log carries one, else the final
92
+ segment of the project directory (for example `Project_lzc`), else the id. A conversation that was
93
+ never renamed therefore reads as its directory rather than as *untitled*.
94
+ - **Selection follows the subtree, and a child can be deleted on its own.** Ticking a parent takes the
95
+ ticked rows below it; ticking one subagent without its parent sends that subagent as a delete root of
96
+ its own and leaves the parent alone. The footer button always states how many conversations the press
97
+ will delete.
98
+
99
+ The dialog carries the same **stop unfinished work first** switch as the single-session one, on by
100
+ default. Confirming runs the delete above once per ticked root; a root that fails is listed and the
101
+ rest still go.
102
+
103
+ ### What blocks a delete, and what does not
104
+
105
+ **Running work blocks it — being open does not.** The gate is DSH's own archive admission, the
106
+ `workspace/session-activity` waterfall: the Agent registry reports a running turn, the job registry
107
+ reports background jobs, the Subagent runtime reports running descendants, Schedule reports active
108
+ reminders. The question is asked **once per Session in the delete set**, because the admission answers
109
+ per Session: a descendant's running turn or background job is not reported when only the target is
110
+ asked. The dialog names what is still running — on each row and in the summary — and offers
111
+ **Stop and delete**, which dispatches `workspace/session-stop` for every busy Session first, exactly
112
+ what `archiveSession(id, { stopActivity: true })` does.
113
+
114
+ A session the Host still holds open (`ctx.sessions` / `ctx.agents`) is deleted anyway. Measured on
115
+ Windows: `rm` succeeds while the append handle is open, the directory entry disappears at once, and
116
+ later appends land in the unlinked file instead of resurrecting it. An earlier version refused such
117
+ sessions with "switch away first", which wrongly blocked conversations that were not on screen.
118
+
119
+ ### Known limits
120
+
121
+ - **Attachment blobs are not removed.** Uploaded images and files live in
122
+ `~/.dsh/attachments/v1/objects/<hash>`, a content-addressed store shared by sessions, and the
123
+ service has no reference counting, so those blobs stay.
124
+ - **The search index looks after itself.** `dsh-session-query-sqlite` is a derived index that
125
+ reconciles against persistence on every search, so a deleted source stops being returned from the
126
+ next search on.
127
+ - **A held-open session may leave one inert cache record.** If the Host still owns the session when
128
+ it is deleted, its disposal can write one more projection checkpoint. That record is never read
129
+ (no log means no session) — a few tens of KB of dead file.
130
+ - **Plugin code changes need a DSH restart.** In a profile whose HMR does not watch module files,
131
+ replacing the code of an installed bundle requires a restart. The client half only needs a page
132
+ refresh.
133
+
134
+ ## Install
135
+
136
+ Everything below goes through DSH's own plugin manager, which accepts an install spec
137
+ (`@deepseek-ai/dsh-plugin-manager`): a registry name, a git host shorthand, a repository URL, a
138
+ tarball, or an absolute local path. Pick whichever route your network allows.
139
+
140
+ ### 1. Straight from GitHub (needs github.com reachable)
141
+
142
+ Spec:
143
+
144
+ ```
145
+ github:Lzcdebear/better-dsh-session-deletetool
146
+ ```
147
+
148
+ or, equivalently:
149
+
150
+ ```
151
+ https://github.com/Lzcdebear/better-dsh-session-deletetool
152
+ ```
153
+
154
+ Give it to DSH:
155
+
156
+ - **In the app:** Settings → Plugins → the install entry, paste the spec. (The Plugin Manager UI and
157
+ the `plugin_manager` tool take exactly the same spec string.)
158
+ - **Through an agent session:** ask the agent to install it — the tool call is `plugin_manager` with
159
+ `action: "install_bundle"` and `target: "github:Lzcdebear/better-dsh-session-deletetool"`.
160
+
161
+ Pin a ref with `#`: `github:Lzcdebear/better-dsh-session-deletetool#v0.1.0`.
162
+
163
+ If the connection check fails, DSH reports a bounded log path. On a network where github.com is not
164
+ reachable, use route 2 or 3 — or point git at your proxy first
165
+ (`git config --global http.proxy http://127.0.0.1:7890`).
166
+
167
+ ### 2. From a local copy (works offline)
168
+
169
+ Download the repository (ZIP or `git clone`), unpack it anywhere, then install the **absolute
170
+ directory**:
171
+
172
+ ```
173
+ plugin_manager { action: "install_bundle", target: "D:\\plugins\\better-dsh-session-deletetool" }
174
+ ```
175
+
176
+ DSH records a `link:` dependency and reloads the profile. This is the route used to develop the
177
+ plugin, and the one to use behind a restrictive network.
178
+
179
+ ### 3. From the release tarball
180
+
181
+ ```
182
+ https://github.com/Lzcdebear/better-dsh-session-deletetool/archive/refs/heads/main.tar.gz
183
+ ```
184
+
185
+ Same install entry as route 1; useful when git is unavailable but HTTPS is not.
186
+
187
+ ### After installing
188
+
189
+ The Host half loads with the profile. The Client half appears after the page is refreshed. The row
190
+ shows up in the session `⋯` menu as **删除会话 / Delete conversation**.
191
+
192
+ To remove the plugin again, use the same manager:
193
+ `plugin_manager { action: "remove_bundle", target: "better-dsh-session-deletetool" }` (the bundle key is the
194
+ package name).
195
+
196
+ ## Usage
197
+
198
+ 1. Open the `⋯` menu on any session row and pick **Delete conversation**.
199
+ 2. The dialog asks the Host for the session's real state and states it: what will be deleted,
200
+ whether the Harness still holds it open, and what is still running.
201
+ 3. It then draws the whole family as a tree to choose from: this conversation on its own row, then its
202
+ **Subagents (n)** and **Forked conversations (n)** blocks, each derived conversation carrying its own
203
+ level inside its block, and a checkbox on every row (title, id tail, state badge). Untick whatever
204
+ you want to keep.
205
+ 4. Confirm. The button states how many conversations will go; if work is running it reads
206
+ **Stop and delete** and stops that work first.
207
+ 5. The rows leave the sidebar at once, and the data is gone from disk.
208
+
209
+ For bulk: press the trash control in the workspace section header, **left of the search icon**, tick
210
+ conversations in the Workspace-sectioned list (ticking a parent brings its children, a child can be
211
+ ticked or unticked on its own), then confirm.
212
+
213
+ ## HTTP surface
214
+
215
+ The Client half reaches the Host over four same-origin `exact` routes on `ctx.webServer`. A
216
+ build-free plain-JavaScript bundle cannot declare a typed `ctx.remote` namespace (that needs
217
+ generated Typert descriptors), so this uses the same transport the community plugin `dshmarket`
218
+ uses.
219
+
220
+ | Route | Method | Purpose |
221
+ |---|---|---|
222
+ | `/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` |
223
+ | `/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 |
224
+ | `/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 |
225
+ | `/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 |
226
+
227
+ All four routes carry their own same-origin gate: `Host` must be loopback, `sec-fetch-site` must not
228
+ be `cross-site`, and a present `Origin` must match `Host`. Another site's page cannot reach them.
229
+
230
+ ## Layout
231
+
232
+ | File | Role |
233
+ |---|---|
234
+ | `host.js` | Host half: the four routes, artifact/accounting/cache removal, the subagent subtree, and the Workspace-sectioned catalog |
235
+ | `client.js` | Client half: the `sidebar.workspaces.session.menu.item` row (order 500), the bulk control, and the two `shell.overlay` dialogs |
236
+ | `cordis.patch.yml` | Inserts the Host row into the profile's layer stack |
237
+ | `test/host.test.mjs` | Route tests over real temporary directories |
238
+ | `icon.svg` | Plugin artwork (the bulk control draws `deleting icon.svg`) |
239
+
240
+ Styles use only host theme tokens (`--dsw-alias-*`), so light and dark both read; the UI is
241
+ localized (English / 简体中文) through the Client locale service.
242
+
243
+ Why the bulk entry is not a slot registration of its own: `sidebar.workspaces` is a `single` slot, so
244
+ a second registrant would **shadow** the shipped session browser instead of sitting beside it. The
245
+ Client half therefore registers in `sidebar.footer.action` (rendering nothing there) and mounts its
246
+ control into the browser's own search cell through a portal and one `MutationObserver`, which
247
+ re-creates the control when React re-renders that header. That insertion carries no authority beyond
248
+ opening the dialog; the two Host routes do the work. If a future harness renames that cell's CSS
249
+ class, the control stops appearing and nothing else changes.
250
+
251
+ ## Development
252
+
253
+ ```sh
254
+ node --test test/host.test.mjs
255
+ ```
256
+
257
+ The tests drive the real `apply()` registration against fake Host services and real temporary
258
+ directories: deletion, the running-work gate and its stop path, the subagent subtree, the inspect
259
+ report, and the refusal paths (bad id, bad body, wrong method, cross-site origin).
260
+
261
+ ## License
262
+
263
+ [MIT](LICENSE)
264
+
265
+ ## Author
266
+
267
+ - Bilibili: <https://space.bilibili.com/220996778>