dsh-sessions-manager 3.5.4 → 3.6.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/README.en.md CHANGED
@@ -10,7 +10,7 @@
10
10
  ![daily compat](https://img.shields.io/github/actions/workflow/status/TOBYCAI/dsh-sessions-manager/compat.yml?branch=main&label=daily-compat&style=flat-square)
11
11
  ![DSH plugin](https://img.shields.io/badge/DSH-plugin-4d6bfe?style=flat-square)
12
12
 
13
- > DSH session manager: archive, move, restore, and inspect sessions from **Settings → 会话管理**; mark unread, move, and delete sessions directly from the **main sidebar**. Deleted sessions go to the recycle bin first and can be restored or permanently purged.
13
+ > DSH session manager: archive, move, restore, and inspect sessions **and their lineage** (subagent folding, branch badges, empty-session cleanup) from **Settings → 会话管理**; mark unread, move, and delete sessions directly from the **main sidebar**, with subagents folded in place under their parents. Deleted sessions go to the recycle bin first and can be restored or permanently purged.
14
14
 
15
15
  A persistent DSH plugin (host + browser halves) covering both the **settings panel** and the **main sidebar**, so frequent session actions don't require opening Settings. It understands both legacy header lists/`readFrom` and the new snapshot/`SessionHandle` API, enabling actions according to the current Runtime's verified capabilities. Purge and migration paths are enabled only when they can be verified — on newer runtimes they are backed by guarded path derivation and write-ownership probes; anything unverifiable is disabled in both UI and Host to prevent false success.
16
16
 
@@ -18,17 +18,18 @@ A persistent DSH plugin (host + browser halves) covering both the **settings pan
18
18
 
19
19
  ### Settings panel: Session Manager
20
20
 
21
- - **Unified panel**: five top-level views — **All / Active / Archived / Starred / Recycle bin** with search across title, session ID, and workspace, plus workspace filters and creation-time/title sorting. A maintenance row below the filters holds the **Auto-archive** and **Storage usage** tools, which expand on demand instead of taking up a view.
21
+ - **Unified panel**: three always-visible top-level views — **All / Active / Archived** while the low-frequency ones (**Starred / Empty / Recycle bin**) are tucked behind an arrow button at the end of the filter row and slide out horizontally on click (they stay expanded automatically while a low-frequency view is selected). Search across title, session ID, and workspace, plus workspace filters and creation-time/title sorting. The result line is a single sentence such as "Showing 5 sessions, 4 more subagents folded under their parents". A maintenance row below the filters holds the **Storage usage** and **Auto-archive** tools, which expand on demand instead of taking up a view.
22
22
  - **Starred sessions**: an always-visible star button on each session row toggles starring optimistically with rollback on failure; the Starred view is orthogonal to DSH's active/archived states and they stack. The star index is plugin-owned (schema v3) and never touches DSH logs; stars are cleaned up automatically when a session is permanently purged.
23
23
  - **Cold-title synchronization**: the sidebar is corrected from the latest log-backed `session/title`, so renamed cold sessions no longer need to be opened before showing their current name.
24
24
  - **Sidebar workspace drag-and-drop**: drag a session onto a workspace heading to move it, with a highlighted drop target, same-workspace protection, failure feedback, and the More → Move session menu retained for keyboard access.
25
25
  - **Archive / Restore**: archive hides a session from the sidebar; restore unarchives it and puts it back into its original workspace group.
26
- - **Move to a workspace**: pick an **existing workspace**, or enter a **new directory path** (auto-created); the new-path mode also lets you open the **native OS directory picker** with a **「浏览…」** button. The session's working directory and log are migrated together, and even an open session can be moved safely.
26
+ - **Move to a workspace**: pick an **existing workspace**, or enter a **new directory path** (auto-created); the new-path mode also lets you open the **native OS directory picker** with a **「浏览…」** button. The session's working directory and log are migrated together. On the legacy runtime (`0.1.2-rc.1`) even an open session can be moved; on the new runtime (`0.1.3-alpha.1`) session logs are **single-writer** — DSH claims write ownership when it opens a session and holds it, so **any session you have opened in DSH** is temporarily unmovable. The error bar states this plainly; in practice switching sessions does not release the ownership, so **restart DSH and move it again**.
27
27
  - **Session details**: expand any session to see **disk usage**, **turns / steps / user·assistant messages / tool calls / image attachments** stats, **tool-usage breakdown**, **search·fetch records**, the **write/edit file list** (already filtered for paths that no longer exist on disk), and **lineage** (parent session / child sessions / subagents).
28
+ - **Lineage disclosure**: the panel's **Grouping** select offers **Lineage (subagents folded, default)** and **Flat (all inline)** views; under lineage grouping subagent sessions fold under their parents. Fork sessions carry a green **"⑂ 分支"** (branch) badge (hover shows the source parent), and empty sessions carry a gray badge with a dedicated **Empty** view for reviewing and cleaning them up. Empty sessions are identified by an **event-type method**: since `0.1.3` the log header grew and every creation writes a lifecycle-metadata frame, so the old size threshold no longer holds — the plugin decodes candidate sessions through the official API and marks one empty only when no content event exists beyond the header (decode failures fall back to non-empty; results are cached per session, making polling nearly free).
28
29
  - **Export**: the detail panel offers two actions — "**Download raw log (ZIP)**" goes straight to DSH's official `session.export` endpoint (subsessions + attachments included, hidden automatically when the persistence backend lacks raw-artifact support); "**Export Markdown**" renders the session into a human-readable transcript (front matter + per-turn sections + user / assistant / tool-call summaries, with no streaming-delta duplication).
29
30
  - **Storage analysis**: a **Storage usage** button in the maintenance row expands on demand to aggregate per-workspace disk usage of session logs (share bars + session counts) and list the Top 10 largest sessions. Read-only statistics that never modify any data; collapsed by default, so nothing is scanned until you open it.
30
31
  - **Auto-archive**: optionally move sessions that have been **inactive for 30 / 60 / 90 days** into **Archived**, **off by default**. The session you are currently using and any starred session (that guard can be turned off) are never auto-archived; it only sets an archived flag and never deletes data, so recovery is always one click away. The check is lazily triggered when you open the panel, at most once per day, and can also be run manually with "Check now".
31
- - **Batch multi-select**: select-all / archive selected / restore selected / delete selected (single confirmation for batch delete).
32
+ - **Batch multi-select**: select-all / archive selected / restore selected / delete selected (single confirmation for batch delete). Once anything is selected, the batch action bar **pins to the top** of the panel (sticky), so a long list never forces you to scroll back up to act; a **"Move selected…"** action adds batch cross-workspace moves (reusing the single-move sheet: existing workspace / new directory / native directory picker). One failing session never blocks the rest — failures are reported one by one.
32
33
 
33
34
  ### Main sidebar: session ⋯ menu augmentation
34
35
 
@@ -36,6 +37,12 @@ A persistent DSH plugin (host + browser halves) covering both the **settings pan
36
37
  - **Move session**: opens a hover submenu to the right listing workspace names; choose one to move the session there. The current workspace is labeled "当前" and disabled.
37
38
  - **Delete session**: deletes the session into the **recycle bin** (not an immediate physical delete); you can restore or permanently purge it from the Session Manager panel.
38
39
 
40
+ ### Main sidebar: subagent folding and branch badges
41
+
42
+ - **Subagent folding**: DSH's upstream sidebar intentionally never renders subagent rows, so the plugin injects them **in place** under their parent session using the official lineage fields (`origin: 'subagent'` / `parentSession` / `delegationDepth`). A **"▸ N 子代理"** badge appears when the parent row is selected or hovered; clicking it expands the indented subagent rows, and clicking a row opens that subagent via the official API. The folded list shrinks as soon as a subagent is deleted.
43
+ - **Branch badges**: sessions created by forking a parent show a green **"⑂ 分支"** (branch) chip when their row is selected or hovered, hinting at their lineage.
44
+ - **Empty sessions**: since `0.1.3` the upstream sidebar doesn't render empty sessions; the plugin hides them too as the fixed default, so managing them lives in the panel's **Empty** view.
45
+
39
46
  ### Main sidebar: status dots
40
47
 
41
48
  A small dot is rendered to the left of each session row. Its color is driven by DSH's native `StateDot` state:
@@ -62,14 +69,40 @@ The plugin hides DSH's own status dot and re-renders it using the palette above,
62
69
  - Permanently deleted sessions stay hidden forever and won't reappear in the sidebar or session list.
63
70
  - Permanent deletion writes a host-side tombstone first, waits for the live Session and persistence controller to retire, removes the log, and rebuilds workspace indexes, preventing stale indexes, reconnects, or an empty Ungrouped section from resurfacing.
64
71
 
72
+ ### Moving an active session (queued moves)
73
+
74
+ Since DSH 0.1.5 every session log has a **single-writer lock**: once a session is open in DSH (even just selected as the current conversation) the agent loop owns its write handle. **The host exposes no "close session" action and no way to release that ownership — it is released only when the DSH process exits** (switching away does not release it, and empirically neither does idle waiting). Forcing the move at that point is dangerous: the live writer keeps appending to the old file and re-materialises a shell at the old path.
75
+
76
+ So held-open sessions get **deferred (queued) moves**:
77
+
78
+ - The request does not fail; the move is recorded in a pending queue (`~/.dsh/sessions-manager/pending-moves.json`) and the plugin reports **"queued" honestly** — it never claims "moved" for a move that did not happen.
79
+ - The queue runs automatically **immediately after plugin startup and retries densely** (0 / 1 / 3 / 6 / 12 / 30 s — it must win the race against the browser opening sessions), then on a light 2-minute fallback, and whenever the host releases a session.
80
+ - Up to 50 entries are kept; an entry that fails 5 times for **non-lock** reasons is dropped and logged (failures within the first 30 s after startup do not count, so a startup race cannot silently drop your queue).
81
+ - Inspect / cancel: `POST /archived-sessions/pending-moves`, `POST /archived-sessions/pending-moves/cancel { sessionIds }`.
82
+ - **To actually move an active session**: request the move (it queues), then **restart DSH and don't open that session first** — it completes a few seconds after startup. Pending entries survive restarts.
83
+
84
+ ### Session format v3 (DSH 0.1.5+)
85
+
86
+ DSH 0.1.5 upgrades the session log format to **v3**; the runtime performs the migration itself while reading:
87
+
88
+ - Migration is **automatic** and **keeps the original files** — so one session directory legitimately holds both `session.v2.jsonl.zstd` and `session.v3.jsonl.zstd`. This plugin always reads the **highest generation**, matching the runtime.
89
+ - Migration is **one-way, with no downgrade read**: after upgrading to 0.1.5 and opening sessions, rolling the runtime back makes those sessions unreadable. Back up `~/.dsh/sessions` before a rollback.
90
+ - Only **supported** old logs are migrated; unknown events, v2 events already carrying reserved v3 tags, or corrupted data are **refused without any repair**. Such sessions surface as `SessionFormatUnsupportedMigrationError` / `SessionFormatError` on read/move — the plugin cannot repair them and reports the error verbatim.
91
+
65
92
  ## Screenshots
66
93
 
67
94
  <details>
68
- <summary>Expand screenshots (settings panel / auto-archive / storage analysis / starred / recycle bin / session details / sidebar menu)</summary>
95
+ <summary>Expand screenshots (sidebar menu / sidebar subagent folding / settings panel / batch move / empty view / auto-archive / storage analysis / starred / recycle bin / session details)</summary>
69
96
 
70
97
  ![Sidebar ⋯ menu: mark unread, move session, delete session](assets/screenshot-session-submenu.png)
71
98
 
72
- ![Settings panel "Session Manager"](assets/screenshot-session-settings.png)
99
+ ![Sidebar subagent folding (" N 子代理" badge + in-place expansion) and the "⑂ 分支" branch badge](assets/screenshot-session-subagent.png)
100
+
101
+ ![Settings panel "Session Manager" (lineage grouping / empty view / collapsed filters)](assets/screenshot-session-settings.png)
102
+
103
+ ![Batch move sheet and the sticky batch action bar](assets/screenshot-session-multimove.png)
104
+
105
+ ![Empty-sessions view (event-type detection)](assets/screenshot-session-empty.png)
73
106
 
74
107
  ![Auto-archive panel (inline sheet in the maintenance bar)](assets/screenshot-session-autoarch.png)
75
108
 
@@ -115,6 +148,7 @@ src/index.js host source (/archived-sessions/* JSON routes)
115
148
  src/client/index.jsx client source (React, settings.section + sidebar DOM augmentation)
116
149
  src/auto-archive.js auto-archive settings (schema v4) + candidate-selection pure functions
117
150
  src/storage-stats.js storage-usage aggregation (pure functions, by workspace / Top N)
151
+ src/lineage.js lineage classification and empty-session detection (pure functions, event-type method)
118
152
  build.mjs esbuild build script (regenerates lib/)
119
153
  lib/index.js pre-built host (ESM)
120
154
  lib/client.js pre-built client (ModuleLoader CJS handshake)
@@ -141,9 +175,13 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
141
175
  | POST | `/archived-sessions/trash/settings` | Read or update automatic cleanup `{ retentionDays }` |
142
176
  | POST | `/archived-sessions/trash/verify` | Verify that recycle-bin logs still exist |
143
177
  | POST | `/archived-sessions/workspaces` | List available target workspaces |
144
- | POST | `/archived-sessions/move` | Move a session to a target workspace `{ sessionId, targetPath }` |
178
+ | POST | `/archived-sessions/move` | Move a session to a target workspace `{ sessionId, targetPath }`; when the session is held open by DSH it returns `{ queued: true }` and queues the move |
179
+ | POST | `/archived-sessions/move-many` | Batch cross-workspace move `{ sessionIds, targetPath }` — one failure never blocks the rest, failures are reported per session, held-open sessions land in `queued`, and the grouping index is rebuilt once at the end |
180
+ | POST | `/archived-sessions/pending-moves` | Inspect the pending-move queue (sessions queued while held open; completed automatically once released) |
181
+ | POST | `/archived-sessions/pending-moves/cancel` | Cancel queued moves `{ sessionIds }` |
145
182
  | POST | `/archived-sessions/details` | Session details (disk / stats / tools / fetch / files / lineage) `{ sessionId }` |
146
- | POST | `/archived-sessions/sidebar-state` | Authoritative sidebar titles, recycle-bin IDs, and permanent-deletion tombstones |
183
+ | POST | `/archived-sessions/sidebar-state` | Authoritative sidebar titles, recycle-bin IDs, permanent-deletion tombstones, and **lineage disclosure** (subagent / branch / empty badges) |
184
+ | POST | `/archived-sessions/lineage-tree` | Recursive subagent tree (feeds the panel's Lineage grouping and the sidebar folding), filtered for recycle-bin and tombstoned sessions |
147
185
  | POST | `/archived-sessions/star/set` | Star / unstar sessions `{ sessionId or sessionIds, starred }` |
148
186
  | GET | `/archived-sessions/export-md?sessionId=` | Single-session Markdown export (human-readable transcript) |
149
187
  | POST | `/archived-sessions/storage` | Storage-usage aggregation (per-workspace ranking + largest sessions) `{ topN }` |
@@ -160,17 +198,19 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
160
198
  - Peer dependencies are listed in `package.json`; `react` and `@deepseek-ai/*` are provided by the DSH runtime.
161
199
  - `0.1.2-rc.1`: the existing read, archive, recycle-bin, permanent-purge, and cross-workspace move paths remain available.
162
200
  - `0.1.3-alpha.1`: snapshot lists and chunked `SessionHandle` read flows are supported. Permanent purge and cross-workspace move are restored via guarded path derivation (see the behavior notes below); they degrade gracefully only when the storage root cannot be confirmed. The panel shows the effective capabilities.
201
+ - `0.1.5-rc.1` (current runtime): the session log format is upgraded to **v3** and this plugin is adapted — it reads the **highest generation** (matching the runtime), tolerates v0/v2/v3 generations side by side, follows the new `SessionHandle.read()` shape (`{ eventState, events }`), moves sessions as **whole-directory moves** (reclaiming superseded-generation copies automatically), and queues moves for sessions DSH holds open (see "Moving an active session" above).
163
202
  - Unverified future runtimes expose only capabilities the plugin can safely identify; method presence alone is not presented as behavioral compatibility.
164
203
 
165
204
  ### Behavior differences and degradations under `0.1.3-alpha.1`
166
205
 
167
206
  - **Recycle-bin restore is verified**: before restoring, the plugin confirms the underlying stored session still exists (live / `stat` / listing, in that order). It returns accurate errors when the session is gone (`DSM_SESSION_MISSING`), was permanently purged (`DSM_SESSION_PURGED`), or the index still lists it but the log file has vanished (`DSM_SESSION_LOG_MISSING`); when the log location cannot be verified the entry is honestly reported as `unverified` instead of silently passing. Restoring a session whose workspace was deleted succeeds and notes that it now lives under "未分组" (ungrouped).
168
207
  - **Permanent purge / empty recycle bin / automatic physical cleanup**: the official public contract offers no delete API, so the plugin keeps the legacy-era semi-official approach — a **three-layer guarded path derivation** starting from the backend instance's storage-root field (root → session directory structure → session-id ownership check), followed by a whole-directory removal verified with the official `stat`. Sessions with an active writer are refused (409). When the derivation fails (e.g. the storage root is unavailable), the actions degrade to disabled with an explicit reason instead of deleting blindly.
169
- - **Cross-workspace move**: the primary path replays events through the official `create` + `append` (writer-ownership probe refuses active sessions, before/after `revision` checks guard against concurrent writes, backup + rollback guarantees no half-moved state on failure); when the backend holds a ghost record for the same id it falls back to relocating the log with a frame0 cwd rewrite (frame count and content verified byte-for-byte). Paths come from the same guarded derivation; the workspace grouping index is rebuilt automatically after the move — no restart needed.
208
+ - **Cross-workspace move**: the primary path replays events through the official `create` + `append` (writer-ownership probe refuses sessions whose write ownership is held by DSH, before/after `revision` checks guard against concurrent writes, backup + rollback guarantees no half-moved state on failure); when the backend holds a ghost record for the same id it falls back to relocating the log with a frame0 cwd rewrite (frame count and content verified byte-for-byte). Paths come from the same guarded derivation; the workspace grouping index is rebuilt automatically after the move — no restart needed. Since runtime `0.1.5` a session may keep **several log generations side by side** (the runtime publishes a newer generation but keeps the older ones: `session.jsonl.zstd` + `session.v2.jsonl.zstd` + `session.v3.jsonl.zstd`), while the runtime insists that one id lives in exactly one project directory — so a move is a **whole-directory move**: the source session directory is renamed out of sight first, the target is materialized and verified, then the source directory is deleted. Superseded older-generation copies left in other project directories by earlier versions are **reclaimed automatically**, so a session can never get stuck as a cross-directory duplicate.
170
209
  - **Purge tombstones never suppress new sessions**: if a session with the same id is recreated later, the tombstone yields automatically and the new session appears normally in lists and the sidebar.
171
210
  - **Metadata-cache change tokens**: on the new runtime the plugin prefers the official `stat`/`list` snapshot `revision` (comparable only within one service instance and one session id) to detect changes; legacy runtimes keep the `(mtime, size)` file fingerprint. Revisions are never persisted into the cross-process title index. List metadata comes straight from `snapshot.header` and titles from the batch projection — a missing title does **not** trigger a full-log decode, avoiding amplification of the known historical-session loading regression in alpha.
172
211
  - **Auto-archive**: the new runtime exposes no reliable "last activity" timestamp; the sweep skips (`no-activity-data`) when idleness cannot be proven, and never archives on a guess.
173
212
  - **Sidebar injection**: DOM / React fiber recognition is centralized in a versioned adapter; when the upstream sidebar shape is no longer recognized, all injections degrade safely and the official sidebar is left untouched. While recognition works, updates are driven incrementally by a MutationObserver with only a low-frequency fallback check.
213
+ - **Lineage disclosure**: subagent sessions are recognized via the official header fields (`origin: 'subagent'`, `parentSession`, `delegationDepth`); since the upstream sidebar renders neither subagent nor empty sessions, the plugin injects subagent rows under their parent and hides empty rows. Empty detection uses the **event-type method** — `0.1.3` always writes one lifecycle-metadata frame at creation (`permission/preset` / `sandbox/mode` / `approval/policy`), which breaks the old size threshold; candidates up to 8KB (compressed) are decoded once through the official API and marked empty only when no content event exists beyond the header, decode failures fall back to non-empty, and results are cached per (session, size) so polling stays nearly free.
174
214
 
175
215
  ## License
176
216
 
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  ![daily compat](https://img.shields.io/github/actions/workflow/status/TOBYCAI/dsh-sessions-manager/compat.yml?branch=main&label=daily-compat&style=flat-square)
11
11
  ![DSH plugin](https://img.shields.io/badge/DSH-plugin-4d6bfe?style=flat-square)
12
12
 
13
- > DSH 会话管理器:在**设置 → 会话管理**里统一归档、移动、恢复、查看详情;在**主页侧边栏**直接标记未读、移动、删除会话。删除先进入回收站,可恢复或彻底清理。
13
+ > DSH 会话管理器:在**设置 → 会话管理**里统一归档、移动、恢复、查看详情与血缘(子代理折叠、分支标记、空白清理);在**主页侧边栏**直接标记未读、移动、删除会话,并把子代理就地折叠进父会话。删除先进入回收站,可恢复或彻底清理。
14
14
 
15
15
  一个 DSH 持久化插件(host + browser 双半),同时覆盖「设置面板」与「主页侧边栏」两个入口,无需打开设置即可完成高频会话操作。插件同时识别旧式 header 列表/`readFrom` 与新版 snapshot/`SessionHandle`,按当前 Runtime 的实际能力启用操作:删除与迁移路径必须可验证才会开放(新版 Runtime 上由守卫式路径推导与写所有权探测保障),无法验证时在界面与 Host 端同步禁用,避免假成功。
16
16
 
@@ -18,17 +18,18 @@
18
18
 
19
19
  ### 设置面板:会话管理
20
20
 
21
- - **统一面板**:顶部提供「全部 / 活动 / 已归档 / 已收藏 / 回收站」五个视图,支持按标题、会话 ID、工作区搜索,按工作区筛选,并按创建时间或标题排序。筛选栏下方另有一行维护栏,收纳「自动归档」与「存储占用」两个按需展开的工具,不占用视图位置。
21
+ - **统一面板**:顶部提供「全部 / 活动 / 已归档」三个常驻视图,低频的「已收藏 / 空白 / 回收站」收进行尾的箭头按钮,点按横向滑出展开(低频视图处于选中态时自动保持展开);支持按标题、会话 ID、工作区搜索,按工作区筛选,并按创建时间或标题排序。结果行合并为一句说明,如「显示 5 个会话,另有 4 个子代理折叠在父会话下」。筛选栏下方的维护栏收纳「存储占用」与「自动归档」两个按需展开的工具,不占用视图位置。
22
22
  - **收藏(星标)**:会话行左侧常驻星标按钮,单击即收藏 / 取消(乐观更新、失败回滚);「已收藏」视图与 DSH 的活动 / 归档状态正交、可叠加;收藏索引为插件自有 schema v3,不触碰 DSH 日志,会话被彻底删除时自动清理。
23
23
  - **冷态标题同步**:侧栏使用日志中最新的 `session/title` 修正冷启动缓存,改名后的会话无需先打开即可显示新名称。
24
24
  - **侧栏跨工作区拖拽**:直接把会话拖到目标工作区标题即可切换工作区;目标高亮、同工作区拦截、失败反馈,并保留“更多 → 移动会话”作为键盘操作入口。
25
25
  - **归档 / 恢复**:归档把会话从侧栏隐藏;恢复取消归档并放回原工作区分组。
26
- - **移动到工作区**:任选**已有工作区**或**新建目录路径**(自动创建),新建目录支持点击 **「浏览…」** 调用系统目录选择窗口。会话的工作目录与日志一起迁移。旧版 Runtime(`0.1.2-rc.1`)下已打开的会话也可移动;新版 Runtime(`0.1.3-alpha.1`)为避免并发写入损坏日志,正在写入的会话会被拒绝移动,待其空闲后重试即可。
26
+ - **移动到工作区**:任选**已有工作区**或**新建目录路径**(自动创建),新建目录支持点击 **「浏览…」** 调用系统目录选择窗口。会话的工作目录与日志一起迁移。旧版 Runtime(`0.1.2-rc.1`)下已打开的会话也可移动;新版 Runtime(`0.1.3-alpha.1`)为会话日志引入了**单写者所有权**——DSH 打开会话时即取得写权限并持有,因此**在 DSH 里打开过的会话**暂时无法移动,错误条会如实说明;实测切换会话不会释放写权限,**重启 DSH 后再移动**即可。
27
27
  - **会话详情**:展开单条会话查看**磁盘占用**、**轮次 / 步骤 / 用户·助手消息 / 工具调用 / 图片附件**统计、**工具使用分布**、**搜索·抓取记录**、**write/edit 写过的文件列表**(已过滤磁盘上已不存在的路径),以及**血统**(父会话 / 子会话 / 子代理)。
28
+ - **血缘分层披露**:面板的「分组」下拉提供**血缘(子代理折叠,默认)**与**平铺(全部并列)**两种视图,血缘分组下子代理会话折叠在其父会话下;分支会话(fork)带绿色「⑂ 分支」标记(悬停显示来源父会话),空白会话带灰色标记并可进入「空白」视图统一查看与清理。空白会话识别采用**事件类型法**:`0.1.3` 起日志头部扩容且创建时恒写一帧生命周期元数据,旧体积阈值失效——插件对候选会话做官方解码精判,头部之外没有任何内容事件才判空白(解码失败按非空兜底,结果按会话缓存,轮询近零成本)。
28
29
  - **导出**:详情面板底部提供两个入口——「**下载原始日志 (ZIP)**」直接走 DSH 官方 `session.export` 端点(含子会话与附件,持久化后端不支持时自动隐藏);「**导出 Markdown**」由本插件把会话渲染为人类可读对话记录(front matter + 按轮分节 + 用户 / 助手 / 工具调用摘要,流式增量不重复)。
29
30
  - **存储占用分析**:维护栏的「存储占用」按钮按需展开,按工作区聚合会话日志的磁盘占用(占比条 + 会话数),并列出占用最大的会话 Top 10。纯只读统计,不修改任何数据;默认收起、展开时才统计,因此不会拖慢会话列表的加载。
30
31
  - **自动归档**:可设为把 **30 / 60 / 90 天未活跃**的会话自动收进「已归档」,**默认关闭**。当前正在使用的会话与已收藏的会话(该保护可关闭)永不自动归档;只做归档标记,不删除任何数据,随时可恢复。检查在打开面板时惰性触发,每天最多一次,也可点「立即检查」手动执行。
31
- - **批量多选**:全选 / 批量归档 / 恢复所选 / 删除所选(批量删除一次二次确认)。
32
+ - **批量多选**:全选 / 批量归档 / 恢复所选 / 删除所选(批量删除一次二次确认)。勾选后批量操作栏**吸顶**钉在面板顶部,列表再长也无需滑回顶部操作;另提供「**移动所选…**」批量跨工作区移动(复用单移动面板:已有工作区 / 新建目录 / 系统目录浏览),单条失败不阻断整批,失败明细逐条回报。
32
33
 
33
34
  ### 主页侧边栏:会话 ⋯ 菜单增强
34
35
 
@@ -36,6 +37,12 @@
36
37
  - **移动会话**:点击后右侧悬浮子菜单列出工作区名称,选择即移动到目标工作区;当前工作区以「当前」标注并置灰。
37
38
  - **删除会话**:删除会先把会话**移入回收站**(非立即物理删除),设置面板可回收站恢复或彻底清理。
38
39
 
40
+ ### 主页侧边栏:子代理折叠与分支标记
41
+
42
+ - **子代理折叠**:DSH 上游侧栏按设计不渲染子代理行,插件按官方血缘字段(`origin: 'subagent'` / `parentSession` / `delegationDepth`)把子代理行**就地注入**父会话之下——「▸ N 子代理」徽标在该行被选中或悬停时出现,点击展开缩进的子代理行,行内点击即通过官方 API 打开对应子代理会话;子代理被删除后折叠列表同步收敛。
43
+ - **分支标记**:由父会话分支(fork)产生的会话,行上会在选中或悬停时显示绿色「⑂ 分支」chip,提示其血缘来源。
44
+ - **空白会话**:`0.1.3` 上游不把空白会话渲染进侧栏,插件侧同步隐藏并固化为默认行为;空白会话的管理入口收敛到设置面板的「空白」视图。
45
+
39
46
  ### 主页侧边栏:状态圆点
40
47
 
41
48
  每行会话左侧显示一个小圆点,颜色直接读取 DSH 原生 `StateDot` 状态,含义如下:
@@ -62,14 +69,40 @@
62
69
  - 已彻底删除的会话会被永久隐藏,不再出现在侧栏与会话管理列表。
63
70
  - 永久删除会先写入后端墓碑,再等待 live Session 与持久化控制器退出、移除日志并重建工作区索引;即使重连或宿主仍有旧索引,也不会重新出现或生成空的「未分组」。
64
71
 
72
+ ### 移动活跃会话(排队移动)
73
+
74
+ DSH(0.1.5 起)给每个会话日志加了**单写者锁**:会话一旦被 DSH 打开(包括只是选中当前会话),写所有权就被 agent-loop 持有。**官方没有提供任何「关闭会话 / 释放所有权」的入口——该所有权只在 DSH 进程退出时释放**(切换会话不释放;实测空闲等待也不会释放)。此时强行搬运是危险的:在写者会继续往旧文件写,并在旧路径重建空壳目录。
75
+
76
+ 因此本插件对「被占用的会话」采用**排队延迟移动**:
77
+
78
+ - 命中占用时**不会失败**,而是登记进待移动队列(`~/.dsh/sessions-manager/pending-moves.json`),并**如实提示「已排队」**——绝不会在没有真正搬运的情况下报「已移动」。
79
+ - 队列的自动执行时机:**插件启动后立即并密集重试**(0 / 1 / 3 / 6 / 12 / 30 秒,必须抢在浏览器打开会话之前)、之后每 2 分钟轻量兜底、以及宿主释放会话时。
80
+ - 队列最多保留 50 条;同一条连续 5 次因**非占用原因**失败会被放弃并记录日志(启动 30 秒内的失败不计入,避免启动竞态丢队列)。
81
+ - 查看与取消:`POST /archived-sessions/pending-moves`、`POST /archived-sessions/pending-moves/cancel { sessionIds }`。
82
+ - **要真正搬走一个活跃会话**:先发起移动(进队列),再**重启 DSH 并先别打开那个会话**——启动后几秒内会自动完成。未完成的项跨重启保留,不会丢。
83
+
84
+ ### 关于会话格式 v3(DSH 0.1.5+)
85
+
86
+ DSH 0.1.5 把会话日志格式升级到 **v3**,由 runtime 自己在读取时完成迁移:
87
+
88
+ - 迁移是**自动**的,且**保留原文件**——所以同一个会话目录里会同时出现 `session.v2.jsonl.zstd` 与 `session.v3.jsonl.zstd`,属官方预期行为;本插件始终按**最高代**读取,与官方一致。
89
+ - 迁移**单向、不支持降级读取**:升级到 0.1.5 并打开过会话之后,把 runtime 回滚到旧版本会导致那些会话无法读取。回滚前请先备份 `~/.dsh/sessions`。
90
+ - 官方只迁移**受支持的旧日志**;遇到未知事件、已带 v3 预留标记的 v2 事件或损坏数据会**拒绝迁移且不做任何修复**。这类会话在插件里会表现为读取/移动报 `SessionFormatUnsupportedMigrationError` / `SessionFormatError`——插件无法代修,只能如实报错(可先确认该会话是否确实异常,再决定保留或删除)。
91
+
65
92
  ## 截图
66
93
 
67
94
  <details>
68
- <summary>展开查看截图(设置面板 / 自动归档 / 存储占用 / 已收藏 / 回收站 / 会话详情 / 侧边栏菜单)</summary>
95
+ <summary>展开查看截图(侧边栏菜单 / 侧栏子代理折叠 / 设置面板 / 批量移动 / 空白视图 / 自动归档 / 存储占用 / 已收藏 / 回收站 / 会话详情)</summary>
69
96
 
70
97
  ![主页侧边栏 ⋯ 菜单(标记未读、移动会话、删除会话)](assets/screenshot-session-submenu.png)
71
98
 
72
- ![设置面板「会话管理」](assets/screenshot-session-settings.png)
99
+ ![主页侧边栏子代理折叠(「▸ N 子代理」徽标 + 就地展开)与「⑂ 分支」标记](assets/screenshot-session-subagent.png)
100
+
101
+ ![设置面板「会话管理」(血缘分组 / 空白视图 / 筛选收纳)](assets/screenshot-session-settings.png)
102
+
103
+ ![批量移动面板与吸顶批量操作栏](assets/screenshot-session-multimove.png)
104
+
105
+ ![空白会话视图(事件类型法识别)](assets/screenshot-session-empty.png)
73
106
 
74
107
  ![自动归档面板(维护栏内联展开)](assets/screenshot-session-autoarch.png)
75
108
 
@@ -115,6 +148,7 @@ src/index.js host 源码(/archived-sessions/* JSON 路由)
115
148
  src/client/index.jsx client 源码(React,settings.section + 侧栏 DOM 增强)
116
149
  src/auto-archive.js 自动归档设置(schema v4)+ 候选判定纯函数
117
150
  src/storage-stats.js 存储占用聚合(纯函数,按工作区 / Top N)
151
+ src/lineage.js 血缘分类与空白会话判定(纯函数,事件类型法)
118
152
  build.mjs esbuild 构建脚本(本地开发时生成 lib/)
119
153
  lib/index.js 预构建 host(ESM)
120
154
  lib/client.js 预构建 client(ModuleLoader CJS handshake)
@@ -141,9 +175,13 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
141
175
  | POST | `/archived-sessions/trash/settings` | 读取或更新自动清理策略 `{ retentionDays }` |
142
176
  | POST | `/archived-sessions/trash/verify` | 校验回收站日志是否仍存在 |
143
177
  | POST | `/archived-sessions/workspaces` | 列出可选目标工作区 |
144
- | POST | `/archived-sessions/move` | 把会话移动到目标工作区 `{ sessionId, targetPath }` |
178
+ | POST | `/archived-sessions/move` | 把会话移动到目标工作区 `{ sessionId, targetPath }`;目标会话正被 DSH 打开时返回 `{ queued: true }` 并登记排队 |
179
+ | POST | `/archived-sessions/move-many` | 批量跨工作区移动 `{ sessionIds, targetPath }`——单条失败不阻断,失败明细逐条回报,被占用的会话进 `queued` 列表,末尾统一重建索引 |
180
+ | POST | `/archived-sessions/pending-moves` | 查看待移动队列(会话被占用时排队,释放后自动完成) |
181
+ | POST | `/archived-sessions/pending-moves/cancel` | 取消排队 `{ sessionIds }` |
145
182
  | POST | `/archived-sessions/details` | 会话详情(磁盘/统计/工具/fetch/文件/血统)`{ sessionId }` |
146
- | POST | `/archived-sessions/sidebar-state` | 返回侧栏权威标题、回收站 ID 与永久删除墓碑 |
183
+ | POST | `/archived-sessions/sidebar-state` | 返回侧栏权威标题、回收站 ID、永久删除墓碑与**血缘分层**(子代理 / 分支 / 空白标记) |
184
+ | POST | `/archived-sessions/lineage-tree` | 递归子代理树(面板血缘分组与侧栏折叠的数据源),已过滤回收站与墓碑 |
147
185
  | POST | `/archived-sessions/star/set` | 收藏 / 取消收藏 `{ sessionId 或 sessionIds, starred }` |
148
186
  | GET | `/archived-sessions/export-md?sessionId=` | 单会话 Markdown 导出(人类可读对话记录) |
149
187
  | POST | `/archived-sessions/storage` | 存储占用聚合(按工作区排行 + 最大的会话)`{ topN }` |
@@ -160,17 +198,20 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
160
198
  - peerDependencies 见 `package.json`;`react`、`@deepseek-ai/*` 由 DSH 运行时提供。
161
199
  - `0.1.2-rc.1`:现有读取、归档、回收站、永久删除和跨工作区移动能力保持可用。
162
200
  - `0.1.3-alpha.1`:支持 snapshot 列表和 `SessionHandle` 只读流程;永久删除与跨工作区移动在缺少已验证安全路径时自动禁用,其余管理能力继续工作。能力以面板实际提示为准。
201
+ - `0.1.5-rc.1`(当前 runtime):会话日志格式升级到 **v3**,本插件已适配——按**最高代**读取(与官方一致)、v0/v2/v3 多代共存、跟随 `SessionHandle.read()` 的新返回结构
202
+ (`{ eventState, events }`)、跨工作区移动按**整目录搬运**并自动回收被取代的旧代副本;被 DSH 打开着的会话改为**排队延迟移动**(见上文「移动活跃会话」)。
163
203
  - 未经验证的未来 Runtime 默认只开放能够识别的安全能力;插件不会用方法存在与否冒充行为兼容。
164
204
 
165
205
  ### `0.1.3-alpha.1` 下的行为差异与降级说明
166
206
 
167
207
  - **回收站恢复需要校验**:恢复前会确认底层会话仍存在(live / `stat` / 列表三级判定)。底层会话已不存在(`DSM_SESSION_MISSING`)、已被彻底删除(`DSM_SESSION_PURGED`)或索引仍在但日志文件消失(`DSM_SESSION_LOG_MISSING`)时返回准确错误;无法核验日志位置时如实标注 `unverified`,不会假装校验通过。恢复到「工作区已删除」的会话会成功并提示其暂归「未分组」。
168
208
  - **彻底删除 / 清空回收站 / 自动物理清理**:官方公共契约未提供删除 API,插件沿用 legacy 时代的半官方路线——从后端实例的存储根目录字段出发做**三层守卫式路径推导**(根目录 → 会话目录结构 → 会话 ID 归属校验),通过后整目录删除并以官方 `stat` 复核;存在活跃写入的会话会被拒绝(409)。推导失败(如无法确认存储根目录)时自动禁用并说明原因,绝不盲删。
169
- - **跨工作区移动**:主路径为官方 `create` + `append` 事件重放(写所有权探测拒绝活跃会话、revision 前后校验防并发写入、备份回滚保证失败不留半移动状态);后端存在同 ID 幽灵记录时回退到 frame0 cwd 改写搬运(帧数与内容逐位校验)。路径同样来自守卫式推导;移动后自动重建工作区分组索引,无需重启。
209
+ - **跨工作区移动**:主路径为官方 `create` + `append` 事件重放(写所有权探测会拒绝**被 DSH 占用写权限**的会话、revision 前后校验防并发写入、备份回滚保证失败不留半移动状态);后端存在同 ID 幽灵记录时回退到 frame0 cwd 改写搬运(帧数与内容逐位校验)。路径同样来自守卫式推导;移动后自动重建工作区分组索引,无需重启。Runtime `0.1.5` 起会话日志会**多代共存**(官方发布新代时保留旧代,如 `session.jsonl.zstd` + `session.v2.jsonl.zstd` + `session.v3.jsonl.zstd`),而官方按「同一 ID 只能落在一个项目目录」校验——因此移动是**整目录搬运**:先把源会话目录整体移开(对官方扫描彻底不可见),目标侧重放校验成功后再删除源目录;顺带**自动回收**其它项目目录里被取代的旧代副本(历史移动残留),避免同一会话永久卡在「跨目录重复」而无法移动或删除。
170
210
  - **彻底删除墓碑不压制新会话**:若同 ID 会话被重新创建,墓碑自动让位,新会话正常出现在列表与侧栏。
171
211
  - **元数据缓存的变更令牌**:新 Runtime 优先使用官方 `stat`/`list` 快照的 `revision`(仅同一 service 实例、同一会话内可比较)判断会话是否变化,旧 Runtime 继续使用 `(mtime, size)` 文件指纹;`revision` 绝不写入跨进程持久索引。列表元数据直接取自 `snapshot.header`,标题走批量投影——没有标题时**不会**自动解码整本日志,避免放大 alpha 已知的历史会话加载性能回退。
172
212
  - **自动归档**:新 Runtime 不提供可靠的「最后活跃时间」,无法证明会话闲置时检查会跳过(`no-activity-data`),绝不基于猜测归档。
173
213
  - **侧栏注入**:DOM / React fiber 识别收敛为可版本化的 adapter;上游侧栏结构不被识别时整体安全停用,不影响官方侧栏本身。识别正常时以 MutationObserver 增量驱动为主,仅保留低频兜底检查。
214
+ - **血缘分层**:子代理会话以官方头部字段识别(`origin: 'subagent'`、`parentSession`、`delegationDepth`);上游侧栏不渲染子代理行与空白会话,插件据此把子代理行就地注入父会话下、把空白行隐藏。空白判定用**事件类型法**——`0.1.3` 创建会话时恒写一帧生命周期元数据(`permission/preset` / `sandbox/mode` / `approval/policy`),体积阈值法失效;插件对压缩体积 ≤8KB 的候选走官方解码精判,头部之外无内容事件才判空白,解码失败按非空兜底,结果按 (会话, 体积) 缓存使轮询近零成本。
174
215
 
175
216
  ## License
176
217