@omdp/dsh-archived-sessions
Archived-sessions manager for the DeepSeek Harness web UI — fork of @muwinds/dsh-archived-sessions (0.2.0), adapted for DSH 0.1.5-rc.1 through 0.2.0-rc.1.
DSH 归档会话管理:在 设置 → 归档会话管理 中查看、释放、删除已归档会话(支持按树删除子会话、清理孤儿会话)。自 0.3.4 起与 DSH 0.1.6 内置的「已归档会话」设置页共存、互不冲突。0.3.6 起 ctx.shell 双时代自适应(run() / execute()),0.1.5-rc.1 → 0.2.0-rc.1 全区间删除可用(0.3.7 追加 rc.2 声明、0.3.8 追加 0.2.0-rc.1 声明)。
English · 中文
中文
这是什么
DSH 的会话可以归档(移到「归档云店」),本插件在设置页提供归档会话的管理界面:
- 列表:标题、会话 ID、所属工作区、磁盘占用、创建时间、是否运行中;
- 释放:把会话从归档集合移回活动列表(不删数据);
- 删除:从硬盘删除会话目录 + 从归档集合移除(两步确认);
- 按树删除:删除主会话时,其下的 subagent 子会话(
parentSession 链)一并删除,不再留孤儿(修复上游 issue #2);
- 孤儿清理:一键扫描并清理「父会话已删除、自己还在盘上」的残留子会话目录;
- 详情:展开查看会话内容(前 100 条消息)。
为什么有这个 fork
上游 @muwinds/dsh-archived-sessions 0.2.0 与 DSH 0.1.5-rc.1 不兼容(作者已一个月未维护):
| 症状 | 根因 |
|---|
| 归档列表全部显示「文件缺失」 | 0.1.5-rc.1 的 sessionPersistence.list() 返回 SessionPersistenceSnapshot[]({header, revision, eventCount?, sizeBytes?}),不再是裸 SessionHeader[];插件按旧形状取值,header.id 变 undefined |
| 「删除」和「释放」行为相同(都只移除归档标记) | 0.1.5-rc.1 抽象服务移除了 locate(meta);插件 persistence.locate(header) 返回 undefined → .path 抛 TypeError → 被 catch 吞掉 → 走 no-artifact 分支,跳过删目录 |
本 fork 的修复:
list() 改为识别快照形状,从 snapshot.header 取值;优先用快照自带的 sizeBytes;
- 不再依赖
locate(),改用 DSH JSONL 后端同款路径编码(encodeSegment / projectKey)自行解析会话目录,并经 fs.resolve 确认存在后才操作;
- 删除前校验目录名必须是会话目录(
session-<uuid> 或裸 UUID),拒绝删除任何非会话路径,杜绝误删;
- 删除按
parentSession 递归收集整棵子树,连同子会话一起删 + 一起移出归档集合;
- 新增
/dsh-archived/orphans 与 /dsh-archived/sweep:扫描/清理孤儿子会话(父会话已不在 persistence 中)。
安装
pnpm add @omdp/dsh-archived-sessions -w
若从 @muwinds/dsh-archived-sessions 迁移:先在 profile 的 package.json 中移除旧依赖(github:MuWinds/dsh-archived-sessions),再安装本包。旧包与新版 API 路由相同(/dsh-archived/*),安装后刷新页面即可。
要求 / Requirements
- DeepSeek Harness 0.2.0-rc.1 / 0.2.0-rc.2(实测版本;0.3.9 起改用三元组区间
>=0.2.0-rc.1 <0.2.1-0,自动覆盖同三元组后续 rc 与正式版 0.2.0)。更早的 0.1.5~0.1.7 系列曾支持(0.3.6 起全兼容),但自 0.3.9 起不再声明——需要在 0.1.7 上运行请装 0.3.8 或更早
@deepseek-ai/dsh >=0.2.0-rc.1 <0.2.1-0(peer,三元组区间;0.3.9 起;语义见下方「兼容性门禁」)
@deepseek-ai/cordis 4.0.1 / 4.0.2 / 4.0.4(peer,逐版本枚举)
@deepseek-ai/dsh-session-persistence-jsonl(可选,随 DSH 自带;缺失时回退到内置路径编码)
ctx.shell 双时代自适应(0.3.6)
删目录那一步是本插件唯一会「真正碰磁盘」的操作,而 ctx.shell 的契约在 DSH 0.1.7 改过名——
两侧方法名不同、且旧方法在新版被整个移除(解包 npm tarball 核对 @deepseek-ai/dsh-shell 的
lib/types/index.d.ts:0.1.5-rc.1/0.1.5-rc.2/0.1.5-rc.3/0.1.6-alpha.1 里
abstract run(...) 存在、execute 为 0 次;0.1.7-rc.1 里 abstract execute(spec): Promise<ShellExecution>
存在、run 为 0 次):
| DSH | 调用链 | 结果 |
|---|
≤ 0.1.6(rc.x / alpha) | resolve(request) → spec → run(spec) → Promise<ShellRunResult> | 只有 run |
≥ 0.1.7 | resolve(request) → spec → execute(spec) → ShellExecution → await execution.result() | 只有 execute |
所以 removeDir() 现在运行期探测:优先 execute()(0.1.7+),否则回退 run()(≤0.1.6),
两者都没有才报错。这样同一个包在整条版本线上都能删。教训:0.3.4 只调 run()(rc.x 可用、
0.1.7 全废),0.3.5 只调 execute()(0.1.7 可用、rc.x 静默失效——typeof shell.run !== "function"
守卫直接抛错,被 UI 收敛成红字「删除失败」),两次都是「只赌一边」。
兼容性门禁(0.3.6 起声明,0.3.9 起改用三元组区间)
当前声明为 "@deepseek-ai/dsh": ">=0.2.0-rc.1 <0.2.1-0"。
该声明由 DSH 自己的
evaluatePluginCompatibility()(dsh-app-boot 的公开导出)在安装与每次启动时校验:命中未实测
的新版本时会整包优雅跳过(stderr 打 skipping profile bundle,DSH 照常启动),命中实测版本
则正常加载。声明从 0.1.7-rc.1 起是因为这个门禁本身是 0.1.7 才引入的(逐版解包核对
dsh-app-boot:0.1.5-rc.2/0.1.5-rc.3/0.1.6-alpha.1/0.1.6-alpha.2/0.1.7-alpha.1/0.1.7-alpha.2
里 evaluatePluginCompatibility 出现 0 次)——旧运行时读到这条 peer
只是不认识的声明,不影响加载,所以老用户不会因为这条声明而失去插件。
0.3.9 的语义变化:从逐版本枚举(0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1)改为三元组区间。
逐版本枚举下每发一个 rc 就要改声明 + 重发包(0.2.0-rc.1 → rc.2 已连续踩两轮,每轮都表现为
「设置项消失、路由 405/404」);而同一 [major,minor,patch] 内的 rc 属同一 API 契约迭代
(实测 rc.1→rc.2 全量 20 包逐文件 SHA256:本插件依赖面只有 package.json 版本号变化),
故实测该三元组首个 rc 即可放行其后续 rc 与正式版。区间最多宽到一个三元组,跨 patch/minor
(如 0.2.1-rc.1)仍需重新核查后新增区间——见 AGENTS.md 规范 3。
⚠️ 上界必须写 <0.2.1-0 而非 <0.2.1:semver 里预发布排在正式版之前,0.2.1-rc.1 < 0.2.1
成立,写 <0.2.1 会漏放行下一个三元组的 rc。
⚠️ 0.3.9 不再声明 0.1.7 线:旧运行时上会被门禁跳过,需要 0.1.7 请装 0.3.8 或更早。
0.3.7 追加 0.1.7-rc.2 的依据:rc.1→rc.2 tarball 逐文件 diff 显示 dsh-shell(本插件删除功能的
唯一 shell 依赖面)的 lib/ 逐字节零变化;再用 rc.2 的门禁跑新声明 → 放行;并在 scratch
profile(dsh@0.1.7-rc.2 + 插件 0.3.6 + 精确版本豁免)上真机实测完整删除链路:
POST /dsh-archived/delete → {"ok":true} + 磁盘目录消失 + 归档集合清空。
API
| 方法 | 请求 | 响应 |
|---|
POST /dsh-archived/list | {} | { items, totalBytes } |
POST /dsh-archived/unarchive | { sessionId } | { ok, changed, archivedSessionIds } |
POST /dsh-archived/delete | { sessionId } | { ok, deleted, sessionId, alsoDeleted[], sizeBytes, warnings[]?, reason? } |
POST /dsh-archived/detail | { sessionId } | { id, createdAt, cwd, parentSession, totalEvents, messageCount, truncated, messages } |
POST /dsh-archived/orphans | {} | { items, totalBytes } |
POST /dsh-archived/sweep | {} | { removed, freedBytes, items, warnings[]? } |
删除(含 sweep)成功后,Host 会向所有客户端广播 api-session/removed,让 DSH 自己的会话列表
立刻丢弃该行;同时释放归档集合与工作区 sessionIds 记账,并删除磁盘目录。若某个清理子步骤
失败,删除本身仍然成功完成,失败原因放在 warnings[] 里返回(避免「磁盘已删但报失败」的误判)。
变更记录
-
0.3.10(2026-10-01):修复「删除归档会话后,它们又回到对话列表,点进去报 session/not-found」。
根因(全链路实测确认)是客户端陈旧行,不是 Host 端删除失败:归档在 DSH 里只是
可见性标记(archivedSessionIds + 客户端 sessionVisible()),它从不出现在会话列表的
过滤逻辑里(dsh-session-query 全库 0 处 archiv*)。客户端 manager.summaries 会一直保留
该行,只被归档集合遮住。删除时旧代码只做「从归档集合移除」——遮罩一撤,幸存的行立刻重新
可见,于是看起来就是「已删除的归档会话又出现了」;点它去读历史,Host 已无该会话 ⇒
session "…" not found (session/not-found)。
旧代码唯一的客户端通知是 evictSessionFromMemory() 里 entry.detach() 顺带触发的
session/disposed → api-session/removed,只有会话仍驻留内存(live)时才会发生;
已归档且未打开的会话通常非 live ⇒ 一个事件都不发,行永久残留到重启。
修复(lib/index.js):
- 新增
announceSessionRemoved(),在每个删除成功分支显式 ctx.emit("api-session/removed", id)
——与 DSH 自己在 session/disposed 上做的完全一致(dsh-api-session-controller 对
api-session/added/removed 的转发即此模式),该事件在 dsh-api-remotes 的
API_REMOTE_FORWARDED_EVENTS 白名单内,客户端 handleSessionRemoved 会走
recordMutation({kind:'remove'}) 真正删行。
因 cordis 的 emit 同步且不隔离(一个监听器抛错会打断后续),emit 用 try/catch 包住,
抛错只记入 warnings,绝不回滚已完成的删除。
- 四个删除分支(无 header / 无路径 /
found:false / 删盘成功)统一收口到 finishRemoval(),
保证「归档集合 + 工作区记账 + 内存驻留 + 客户端通知」四件事不漏。
- 修
location.found 被忽略的 bug:原 :544 只判断 path 非空,而 resolveSessionLocation
在没有任何候选根命中时返回的仍是拼出来的 path(found:false,其文档注释已写明此时
"deletion then prunes the archive id only")。旧代码会拿这个猜测路径去 rm -rf,必然失败 ⇒
报「删除失败」且归档 id 永远卡住。现按 found 判定。
- 释放工作区记账(新增
pruneWorkspaceAccounting()):dsh-workspace 明确写着归档
故意不碰 sessionIds("Archiving never touches workspace accounting"),所以删除必须自己
释放,否则注册表永久记账一个盘上已无、header 也读不回来的 id(bootstrap 对
sessionPaths 缺失的 id 直接 continue,只能手改 workspace.json 才能清)。
走 → ,它是的官方写路径:在域写链上判成员、
无变化时用内部哨兵中止(不写盘、不发变更),并按 canonical-cwd 头索引重滤
——正是 要求的「一个 id 不得被两个工作区同时记账」。
English
What is this
DSH sessions can be archived; this plugin adds an archived-session manager under Settings → 归档会话管理 (Archived Sessions Manager):
- List: title, session ID, workspace, disk usage, created time, running state;
- Release (释放): move a session out of the archive set back to active (no data deleted);
- Delete (删除): delete the session directory from disk + remove from the archive set (two-step confirm);
- Tree delete: deleting a main session also deletes its subagent children (
parentSession chain) — fixes upstream issue #2;
- Orphan sweep: scan and clean leftover subagent dirs whose parent session is already gone;
- Detail: expand to read the session content (first 100 messages).
Why this fork
Upstream @muwinds/dsh-archived-sessions 0.2.0 is incompatible with DSH 0.1.5-rc.1 (author unmaintained for a month):
| Symptom | Root cause |
|---|
| All archived items show 文件缺失 (missing) | 0.1.5-rc.1 sessionPersistence.list() returns SessionPersistenceSnapshot[] ({header, revision, eventCount?, sizeBytes?}), not bare SessionHeader[]; the plugin read the old shape, so header.id was undefined |
| Delete behaves like Release (both only prune the archive id) | 0.1.5-rc.1 removed locate(meta) from the abstract service; persistence.locate(header) returned undefined → .path threw TypeError → swallowed by catch → no-artifact branch skipped the disk deletion |
Fixes in this fork:
list() recognizes the snapshot shape and reads snapshot.header; prefers the snapshot's sizeBytes;
- No longer uses
locate(); resolves the session directory with the same path encoding as the DSH JSONL backend (encodeSegment / projectKey), confirmed via fs.resolve;
- Refuses to delete anything whose directory name is not a session dir (
session-<uuid> or bare UUID) — no accidental deletions;
- Delete collects the whole
parentSession subtree recursively and removes children together (dirs + archive set);
- New
/dsh-archived/orphans and /dsh-archived/sweep to scan/clean orphan subagent sessions (parent no longer in persistence).
Install
pnpm add @omdp/dsh-archived-sessions -w
Migrating from @muwinds/dsh-archived-sessions: remove the old dependency (github:MuWinds/dsh-archived-sessions) from the profile package.json first, then install this package. Same API routes (/dsh-archived/*); refresh the page after installing.
Requirements
- DeepSeek Harness 0.2.0-rc.1 / 0.2.0-rc.2 (tested; since 0.3.9 the peer uses the triple range
>=0.2.0-rc.1 <0.2.1-0, which automatically covers later rc builds in the same triple and the stable 0.2.0). The earlier 0.1.5–0.1.7 line was supported (0.3.6+) but is no longer declared as of 0.3.9 — use 0.3.8 or older if you need it on 0.1.7.
@deepseek-ai/dsh >=0.2.0-rc.1 <0.2.1-0 (peer, triple range; since 0.3.9)
@deepseek-ai/cordis 4.0.1 / 4.0.2 / 4.0.4 (peer, enumerated per version)
@deepseek-ai/dsh-session-persistence-jsonl (optional, ships with DSH; falls back to the built-in path encoding when absent)
Dual-era ctx.shell adaptation (0.3.6)
The directory-removal step is the only place this plugin actually touches the disk, and ctx.shell's
contract was renamed in DSH 0.1.7 — the two eras expose different methods, and the old one is removed
outright in the new one (verified by unpacking each @deepseek-ai/dsh-shell npm tarball and reading
lib/types/index.d.ts: abstract run(...) exists and execute appears 0 times in
0.1.5-rc.1/0.1.5-rc.2/0.1.5-rc.3/0.1.6-alpha.1; 0.1.7-rc.1 has
abstract execute(spec): Promise<ShellExecution> and run appears 0 times):
| DSH | Call chain | Result |
|---|
≤ 0.1.6 (rc.x / alpha) | resolve(request) → spec → run(spec) → Promise<ShellRunResult> | only run |
≥ 0.1.7 | resolve(request) → spec → execute(spec) → ShellExecution → await execution.result() | only execute |
removeDir() now probes at runtime: prefer execute() (0.1.7+), fall back to run() (≤0.1.6), and only
error out when neither exists — so one build deletes across the entire line. The lesson: 0.3.4 called
only run() (fine on rc.x, dead on 0.1.7) and 0.3.5 called only execute() (fine on 0.1.7, silently
dead on rc.x, where the typeof shell.run !== "function" guard threw immediately and the UI surfaced
it as the red "删除失败" banner) — both were bets on a single era.
Compatibility gate (declared since 0.3.6, triple range since 0.3.9)
The current declaration is "@deepseek-ai/dsh": ">=0.2.0-rc.1 <0.2.1-0". DSH's own
evaluatePluginCompatibility() (a public export of dsh-app-boot) checks it at install time and on
every boot: on an untested newer runtime the whole bundle is gracefully skipped (stderr prints
skipping profile bundle; DSH still boots), and on the tested runtime it loads normally.
0.3.9 semantic change: the peer moved from per-version enumeration
(0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1) to a triple range. Enumeration forces a declaration change
plus a republish for every new rc (0.2.0-rc.1 → rc.2 hit this twice, each time as "settings item
gone, route 404/405"), whereas rc builds inside one [major,minor,patch] are iterations of the same
API contract (verified: rc.1 → rc.2 across 20 packages is byte-identical except package.json
version strings for everything this plugin touches). So testing the first rc of a triple clears its
later rc builds and the stable release. A range never spans more than one triple — crossing a
patch/minor (e.g. 0.2.1-rc.1) still requires a fresh check and a new segment (AGENTS.md rule 3).
⚠️ The upper bound must be <0.2.1-0, never <0.2.1: semver sorts prereleases before the stable
release, so 0.2.1-rc.1 < 0.2.1 holds and <0.2.1 would leak the next triple's rc.
⚠️ 0.3.9 drops the 0.1.7 line — it will be gated off there; use 0.3.8 or older on 0.1.7.
The 0.1.7-rc.2 addition (0.3.7) is based on: a file-by-file tarball diff showing dsh-shell's lib/
(this plugin's only shell surface) is byte-identical rc.1 → rc.2; running the rc.2 gate against the
new declaration → pass; and a live end-to-end delete test on a scratch profile
(dsh@0.1.7-rc.2 + 0.3.6 + exact-version exemption): POST /dsh-archived/delete {"sessionId":"session-f71d25a2-…"} → {"ok":true,"deleted":true} + directory gone from disk +
archive set emptied + list returning {items:[]} (the test session was restored from backup).
API
| Method | Request | Response |
|---|
POST /dsh-archived/list | {} | { items, totalBytes } |
POST /dsh-archived/unarchive | { sessionId } | { ok, changed, archivedSessionIds } |
POST /dsh-archived/delete | { sessionId } | { ok, deleted, sessionId, alsoDeleted[], reason? } |
POST /dsh-archived/detail | { sessionId } | { id, createdAt, cwd, parentSession, totalEvents, messageCount, truncated, messages } |
POST /dsh-archived/orphans | {} | { items, totalBytes } |
POST /dsh-archived/sweep | {} | { removed, freedBytes, items } |
Changelog
-
0.3.9 (2026-09-30): peer declaration switched to the triple range >=0.2.0-rc.1 <0.2.1-0 (zero code changes). Background: after 0.2.0-rc.2 shipped, the gate blocked 0.3.8 the same way it had blocked 0.3.7 on 0.2.0-rc.1 — the third consecutive round of the same failure (settings item gone, POST /dsh-archived/list → 405). The root cause was never incompatibility but per-version enumeration going stale on every rc. Verification: full 20-package file-by-file SHA256 diff 0.2.0-rc.1 → rc.2 — dsh-shell / dsh-session / dsh-session-persistence-jsonl / dsh-session-query / dsh-workspace are all byte-identical (only package.json version strings changed); the real gate over the full matrix gives 0.2.0-rc.1 / rc.2 / rc.3 / rc.9 / stable 0.2.0 all PASS and 0.2.1-rc.1 / 0.3.0-rc.1 all BLOCK. ⚠️ This narrows support: the 0.1.7 line is no longer declared — install 0.3.8 if you need it there.
-
0.3.8 (2026-09-28): adds DSH 0.2.0-rc.1 support (peer enumeration 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1, zero code changes). Background: after the desktop app moved to 0.2.0-rc.1 the gate skipped the bundle declared only up to 0.1.7-rc.2 (the settings-page item disappeared and POST /dsh-archived/list returned 405 with the /dsh-archived/ prefix unregistered — a graceful skip, not a crash). Verification: full file-by-file tarball diff 0.1.7-rc.2 → 0.2.0-rc.1 over every lib/ file — nothing this plugin uses broke: dsh-shell (its only shell surface: the execute/resolve/result trio) is byte-identical, and dsh-session's 5 changed files are additive (new exports ToolCallRecovery / TOOL_NOT_STARTED / TOOL_OUTCOME_UNKNOWN; SessionStore and every pre-existing export kept — the export-set diff shows additions only, no removals). Also: the 0.2.0-rc.1 gate the new manifest and the old 0.3.7 one; a sandbox with the official + isolated ran → plugin present in the tree, no gate skip; the artifact was re-checked to carry the new enumeration.
License
MIT — original by MuWinds (upstream package), fork maintained by XJungit.