Chuyển đến nội dung chính

dsh-archived-sessions

Đã xác minh

@omdp/dsh-archived-sessions · v0.3.10 · MIT · Giao diện web

Archived-sessions manager for the DeepSeek Harness web UI: list / unarchive / delete / detail from Settings. · DSH 归档会话管理:设置页查看、释放、删除已归档会话(含按树删除子会话与孤儿清理)。

Cài đặt

dsh plugin add @omdp/dsh-archived-sessions

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.

Thẻ

Readme

@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 的修复:

  1. list() 改为识别快照形状,从 snapshot.header 取值;优先用快照自带的 sizeBytes;
  2. 不再依赖 locate(),改用 DSH JSONL 后端同款路径编码(encodeSegment / projectKey)自行解析会话目录,并经 fs.resolve 确认存在后才操作;
  3. 删除前校验目录名必须是会话目录(session-<uuid> 或裸 UUID),拒绝删除任何非会话路径,杜绝误删;
  4. 删除按 parentSession 递归收集整棵子树,连同子会话一起删 + 一起移出归档集合;
  5. 新增 /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([email protected] + 插件 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):

    1. 新增 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,绝不回滚已完成的删除。
    2. 四个删除分支(无 header / 无路径 / found:false / 删盘成功)统一收口到 finishRemoval(), 保证「归档集合 + 工作区记账 + 内存驻留 + 客户端通知」四件事不漏。
    3. 修 location.found 被忽略的 bug:原 :544 只判断 path 非空,而 resolveSessionLocation 在没有任何候选根命中时返回的仍是拼出来的 path(found:false,其文档注释已写明此时 "deletion then prunes the archive id only")。旧代码会拿这个猜测路径去 rm -rf,必然失败 ⇒ 报「删除失败」且归档 id 永远卡住。现按 found 判定。
    4. 释放工作区记账(新增 pruneWorkspaceAccounting()):dsh-workspace 明确写着归档 故意不碰 sessionIds("Archiving never touches workspace accounting"),所以删除必须自己 释放,否则注册表永久记账一个盘上已无、header 也读不回来的 id(bootstrap 对 sessionPaths 缺失的 id 直接 continue,只能手改 workspace.json 才能清)。 走 registry.list() → entity.detachSession(),它是竞态安全的官方写路径:在域写链上判成员、 无变化时用内部哨兵中止(不写盘、不发变更),并按 canonical-cwd 头索引重滤 sessionIds ——正是 validateStoredState 要求的「一个 id 不得被两个工作区同时记账」。
    5. /dsh-archived/sweep 同样收口(原实现删了目录却从不清归档集合、也从不通知客户端, 会造出与 1 完全相同的鬼行);返回体新增可选 warnings[]。

    修复(lib/client.js):ArchivedSessionsPage() 原本不接收任何参数,函数体里的 ctx.workspaces / ctx.sessions / ctx.timer 全是自由变量 ⇒ 组件作用域内根本没有 ctx ⇒ ReferenceError,又被静默 try/catch 吞掉、promise 直接丢弃。也就是说 refreshViews() 从未真正执行过(连带 reloadAfterAction() 的刷新腿、armDelete 的 5 秒 自动取消也全是死代码)。现由 apply() 通过 slot props 把 ctx 传入组件(与 DSH 官方客户端插件 把 t/renderSlot 等按 props 注入的写法一致),refreshViews() 改为真正 await ctx.sessions.refresh() 并在失败时 console.warn(不再静默),同时删掉客户端并不存在的 ctx.workspaces.refresh() 调用。它作为兜底与 Host 事件互补:refreshList() 的 mergeOrderedBaseline 会丢弃 baseline 中不存在的 id,且拉取期间记录的 mutation 会在基线到达后 重放(removedSincePull),所以补拉不会把已删除的行复活。

    其它:package.json 的 dsh.client.inject 移除了 @deepseek-ai/dsh-client-runtime ——该包在 DSH 里根本不存在(289 个 @deepseek-ai/* 包中无此目录,仅 dsh-invariants 的 README 正文提到过这个名字)。该字段按官方文档只用于激活顺序/预取元数据,不参与 cordis 注入;保留无效名只会误导。

    验证(scratch 独立 DSH_HOME,真机 dsh scratchweb,非 live 的归档会话=报告场景):

    旧代码 新代码
    发给客户端的 api-session/removed 0(行残留,即本 bug) 1
    归档集合 已清 已清
    工作区记账(workspace.json 落盘复核) 仍记账已删 id 已释放
    磁盘目录 已删 已删

    另用真实 mergeOrderedBaseline 跑客户端逻辑仿真,确认「remove 事件」与「await refresh」两条腿 都能删掉该行,且拉取期间到达的 remove 不会被基线覆盖。

  • 0.3.9(2026-09-30):peer 声明改用三元组区间 >=0.2.0-rc.1 <0.2.1-0(代码零改动)。 背景:0.2.0-rc.2 发布后门禁又把只声明到 0.2.0-rc.1 的 0.3.8 拦下——连续第三轮同型故障 (设置项消失、POST /dsh-archived/list → 405),根因不是不兼容,而是逐版本枚举在 rc 迭代下必然过时。 核查:0.2.0-rc.1 → rc.2 全量 20 包逐文件 SHA256——dsh-shell/dsh-session/ dsh-session-persistence-jsonl/dsh-session-query/dsh-workspace 的 lib/ 全部逐字节零变化 (仅 package.json 版本号)。门禁矩阵实测:0.2.0-rc.1/rc.2/rc.3/rc.9/正式版 0.2.0 全 PASS, 0.2.1-rc.1/0.3.0-rc.1 全 BLOCK。 ⚠️ 本条同时收窄支持面:不再声明 0.1.7 线,需要在 0.1.7 上运行请装 0.3.8。

  • 0.3.8(2026-09-28):追加 DSH 0.2.0-rc.1 支持(peer 枚举 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1,代码零改动)。 背景:DSH 桌面端升到 0.2.0-rc.1 后,门禁把只声明到 0.1.7-rc.2 的 0.3.7 拦下(设置页里本插件整项消失、 POST /dsh-archived/list → 405 且 /dsh-archived/ 前缀未注册;是优雅跳过而非崩溃)。 核查:0.1.7-rc.2 → 0.2.0-rc.1 全量 tarball 逐文件 diff——本插件依赖面 零破坏: dsh-shell(唯一 shell 依赖面,execute/resolve/result 三件套)lib/ 逐字节零变化; dsh-session 的 5 个变化文件为附加式(新增 ToolCallRecovery / TOOL_NOT_STARTED / TOOL_OUTCOME_UNKNOWN,SessionStore 等既有导出全保留,导出集合比对只有「新增」无「删除」)。 另:0.2.0-rc.1 门禁对新声明判放行、对 0.3.7 旧声明判拦截;沙箱安装官方 @deepseek-ai/[email protected] + 独立 DSH_HOME 跑 --dump-config → 插件入树、零拦截; npm pack 产物复核含新枚举。

  • 0.3.7(2026-09-25):追加 DSH 0.1.7-rc.2 支持(peer 枚举 0.1.7-rc.1 || 0.1.7-rc.2,代码零改动)。 背景:DSH 桌面版 0.1.7-rc.2 上线后,门禁把只声明 0.1.7-rc.1 的 0.3.6 拦下(插件列表「异常」)。 核查:dsh-shell 的 lib/ 在 rc.1→rc.2 逐字节零变化(本插件唯一 shell 依赖面);rc.2 门禁执行 新声明 → 放行;scratch profile([email protected] + 0.3.6 + 豁免)真机删除实测通过—— POST /dsh-archived/delete {"sessionId":"session-f71d25a2-…"} → {"ok":true,"deleted":true} + 磁盘目录消失 + archivedSessionIds 清空 + list 回 {items:[]}(测试会话已从备份还原)。

  • 0.3.6(2026-09-24):ctx.shell 双时代自适应 + 声明 DSH 版本支持。

    1. 修复 0.3.5 对 rc.x 的静默回归:0.3.5 改用 execute() 修好了 0.1.7,但 run() 在 0.1.5/0.1.6 上才是唯一存在的方法,于是 0.3.5 在那些版本上每次删除都抛 shell executor unavailable; cannot delete from disk(0.3.4 用 run() 反而是 rc.x 可用、 0.1.7 全废)。0.3.6 改为运行期探测:优先 execute()(0.1.7+,需再 await execution.result()), 否则回退 run()(≤0.1.6),两者皆无才报错 ⇒ 0.1.5-rc.1 → 0.1.7-rc.1 全区间删除可用。 依据:逐版解包 @deepseek-ai/dsh-shell 的 lib/types/index.d.ts(见上方「ctx.shell 双时代自适应」)。
    2. 新增 @deepseek-ai/dsh peer 声明 0.1.7-rc.1(逐版本枚举,语义见上方「兼容性门禁」)。
    3. 文档同步说明 0.3.5 的 rc.x 回归——避免有人把 0.3.5 当作「rc.x 也能用」的版本。
  • 0.3.5(2026-09-24):适配 DSH 0.1.7-rc.1 —— 修复"删除失败"。0.1.7 的 ctx.shell 是「 resolve(request) → spec / execute(spec) → ShellExecution / execution.result()」三件套, 没有 run()(0.1.5/0.1.6 的 run(spec) 已移除)。旧代码 removeDir() 用 typeof shell.run !== "function" 做前置检查,在 0.1.7 上必然抛出 shell executor unavailable; cannot delete from disk —— 每个会话目录都在真正碰磁盘之前就被拒, UI 表现为红色「删除失败 N 个会话: session-…」。修复:改用 resolve + execute + result(); 任一执行异常都包成明确的「删除失败」错误。同时把 peer 的 @deepseek-ai/cordis: ^4.0.1 范围改为逐版本枚举 4.0.1 || 4.0.2 || 4.0.4(本仓库规范 3:只声明实测过的版本,不用开放范围)。

    ⚠️ 本版的代价:execute() 在 0.1.5/0.1.6 上不存在 ⇒ rc.x 上删除静默失效,由 0.3.6 的 双时代探测修回。

  • 0.3.4(2026-09-15):适配 DSH 0.1.6-alpha.1——DSH 0.1.6 起在 web-app 内置了原生「已归档会话」设置页(@deepseek-ai/dsh-client-ui-settings-unarchive-sessions),它在 settings.section 槽位注册的 id 恰为 archived-sessions,与本插件旧 id 相同 → slot 冲突,整个 Web UI 启动报「Failed to load plugins」被拦截(运行时二分实测:去掉本插件即恢复)。修复:本插件 slot id 改为唯一的 omdp-archived-sessions、导航标签改为「归档会话管理」,与原生项共存(原生只有查看+恢复,删除/按树删除/孤儿清理仍是本插件能力)。其余 API 面(sessionPersistence.list/workspaceRegistry/sessionQuery/shell.run/fs/jsonl 路径编码)对 0.1.6-alpha.1 源码核查零差异。

  • 0.3.3(2026-09-10):修复 0.3.2 的删除回归——0.3.2 把会话根目录改为 DSH_HOME 推导时拼出了混合分隔符路径(C:\Users\xj\.dsh/sessions/...),而删除前校验 assertSessionDirName 的 basename 提取对混合分隔符失效(先按 / 切再按 \ 切,把名字切成残缺片段),导致所有删除被"拒绝删除非会话目录"拦截。修复:① basename 提取改为按分隔符整体切分;② 根目录统一为 /。删除/孤儿清理恢复正常。

  • 0.3.2(2026-09-10):修两处 fork 遗留——① client 半区的模块 id 仍是 @muwinds/dsh-archived-sessions(未随包名改),浏览器端按 @omdp/... 找不到模块、设置页不显示;② 会话根目录改为从 DSH_HOME 环境变量推导(<DSH_HOME>/sessions,缺省 ~/.dsh/sessions),不再硬编码本机路径。

  • 0.3.1(2026-09-10):修复 0.3.0 的发布事故——0.3.0 的 tarball 里没有 lib/(仓库 .gitignore 的 **/lib/ 规则把源码吞了,npm 只打包到 4 个文件),安装后插件加载失败会拖垮 DSH;0.3.1 补回 lib/index.js + lib/client.js(发布前已核对 tarball 内容)。

  • 0.3.0(2026-09-10):fork 自 0.2.0;适配 DSH 0.1.5-rc.1(list 快照形状 + 路径自解析);按树删除子会话;孤儿清理;删除路径安全校验。


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:

  1. list() recognizes the snapshot shape and reads snapshot.header; prefers the snapshot's sizeBytes;
  2. No longer uses locate(); resolves the session directory with the same path encoding as the DSH JSONL backend (encodeSegment / projectKey), confirmed via fs.resolve;
  3. Refuses to delete anything whose directory name is not a session dir (session-<uuid> or bare UUID) — no accidental deletions;
  4. Delete collects the whole parentSession subtree recursively and removes children together (dirs + archive set);
  5. 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 ([email protected] + 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 passes the new manifest and blocks the old 0.3.7 one; a sandbox with the official @deepseek-ai/[email protected] + isolated DSH_HOME ran --dump-config → plugin present in the tree, no gate skip; the npm pack artifact was re-checked to carry the new enumeration.

  • 0.3.7 (2026-09-25): adds DSH 0.1.7-rc.2 support (peer enumeration 0.1.7-rc.1 || 0.1.7-rc.2, zero code changes). Background: the DSH desktop app ships runtime 0.1.7-rc.2, so the gate skipped 0.3.6's exact-rc.1 declaration (plugin list badge 异常). Verification: dsh-shell's lib/ is byte-identical rc.1 → rc.2 (this plugin's only shell surface); the rc.2 gate passes the new declaration; and a live delete test on a scratch profile ([email protected] + 0.3.6 + exemption) succeeded end-to-end — POST /dsh-archived/delete {"sessionId":"session-f71d25a2-…"} → {"ok":true,"deleted":true} + directory gone + archive set emptied + list → {items:[]} (test session restored from backup).

  • 0.3.6 (2026-09-24): dual-era ctx.shell adaptation + declared DSH version support.

    1. Fixes a silent rc.x regression from 0.3.5: 0.3.5 switched to execute() and thereby fixed 0.1.7, but run() is the only method that exists on 0.1.5/0.1.6, so on those versions 0.3.5 threw shell executor unavailable; cannot delete from disk on every delete (0.3.4's use of run() was conversely fine on rc.x and dead on 0.1.7). 0.3.6 probes at runtime: prefer execute() (0.1.7+, awaiting execution.result()), else fall back to run() (≤0.1.6), and only error when neither exists ⇒ deletes work across 0.1.5-rc.1 → 0.1.7-rc.1. Basis: unpacked @deepseek-ai/dsh-shell lib/types/index.d.ts per version (see "Dual-era ctx.shell adaptation" above).
    2. Added the @deepseek-ai/dsh peer declaration 0.1.7-rc.1 (enumerated per version; see "Compatibility gate" above).
    3. Documented 0.3.5's rc.x regression so nobody treats 0.3.5 as "fine on rc.x".
  • 0.3.5 (2026-09-24): DSH 0.1.7-rc.1 support — fixes the "删除失败" (delete failed) banner. 0.1.7's ctx.shell is the trio resolve(request) → spec / execute(spec) → ShellExecution / execution.result(); there is no run() any more (0.1.5/0.1.6 had run(spec)). The old removeDir() guarded on typeof shell.run !== "function" and therefore always threw shell executor unavailable; cannot delete from disk on 0.1.7 — every session directory was rejected before touching the disk, surfacing in the UI as the red banner "删除失败 N 个会话: session-…". Fix: use resolve + execute + result(); any execution error is wrapped into an explicit delete-failure error. Also changed the peer from the range @deepseek-ai/cordis: ^4.0.1 to per-version enumeration 4.0.1 || 4.0.2 || 4.0.4 (repo rule 3: only declare actually-tested versions, never open ranges).

    ⚠️ What this version cost: execute() does not exist on 0.1.5/0.1.6 ⇒ deletes silently stopped working on rc.x, fixed again by 0.3.6's dual-era probe.

  • 0.3.4 (2026-09-15): adapted to DSH 0.1.6-alpha.1 — since DSH 0.1.6 the web-app ships a native archived-sessions settings page (@deepseek-ai/dsh-client-ui-settings-unarchive-sessions) that registers the settings.section slot with id archived-sessions — exactly the id this plugin used → slot collision, and the whole Web UI boot failed with a "Failed to load plugins" screen (verified at runtime by bisecting plugin subsets: removing this plugin restores boot). Fix: this plugin's slot id is now the unique omdp-archived-sessions and its nav label is "归档会话管理", coexisting with the native entry (the native page only lists/restores; delete, tree-delete and orphan sweep remain this plugin's features). All other DSH surfaces used by the plugin (sessionPersistence.list/workspaceRegistry/sessionQuery/shell.run/fs/jsonl path encoding) verified byte- or removal-free against 0.1.6-alpha.1.

  • 0.3.3 (2026-09-10): fixes a 0.3.2 delete regression — 0.3.2 derived the session root from DSH_HOME with a mixed-separator path (C:\Users\xj\.dsh/sessions/...), and the pre-delete guard assertSessionDirName's basename extraction broke on mixed separators (it sliced by / then by \, producing a truncated fragment), so every delete was rejected with "拒绝删除非会话目录". Fixed: ① basename extraction now splits on either separator; ② roots are normalized to /. Delete and orphan sweep work again.

  • 0.3.2 (2026-09-10): fixes two fork leftovers — ① the client half's module id was still @muwinds/dsh-archived-sessions (not renamed with the package), so the browser could not find the module and the settings page never rendered; ② the session root is now derived from the DSH_HOME environment variable (<DSH_HOME>/sessions, falling back to ~/.dsh/sessions) instead of a hard-coded local path.

  • 0.3.1 (2026-09-10): fixes a 0.3.0 release accident — the 0.3.0 tarball contained no lib/ (the repo's **/lib/ gitignore rule swallowed the source, so npm packed only 4 files); installing it broke the plugin load and could take DSH down. 0.3.1 restores lib/index.js + lib/client.js (tarball contents verified before publishing).

  • 0.3.0 (2026-09-10): forked from 0.2.0; DSH 0.1.5-rc.1 support (snapshot list shape + self path resolution); tree delete; orphan sweep; deletion path safety check.

License

MIT — original by MuWinds (upstream package), fork maintained by XJungit.