DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wjackiedev /

wjackiedev/dsh-session-manager

Verified

Archived-session manager for the DeepSeek Harness Web GUI — restore and permanently delete session logs.

★ 0 Stars0 Forks2 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@7d381c69

dsh-session-manager

CI License: MIT Node PRs Welcome

DeepSeek Harness Web GUI 的会话管理插件:在 设置 → 会话管理 里列出全部会话(含已归档),支持筛选、归档/恢复,以及把会话的本地日志目录物理删除。

harness 自带的会话侧栏只能「归档」(单向隐藏),没有取消归档,也没有删除。本插件补齐:

  • 全量列表 —— harness 的会话列表本身包含已归档会话(侧栏是用归档集过滤掉的),所以这里能一并列出,并给出「已归档」标记。
  • 筛选 / 搜索 —— 全部 / 活动 / 已归档 / 运行中 / 子代理 五种筛选,加上按标题、目录、id、工作区的本地子串过滤。
  • 归档与恢复 —— 归档走 harness 的注册表归档动作;恢复改注册表的归档集合(domain/changed 会让侧栏实时同步)。恢复前会确认会话的日志仍在磁盘上:文件已被删除的会话拒绝恢复,否则会把一个点开就 ENOENT 的空壳放回侧栏(见下)。
  • 删除 —— 物理移除会话的本地日志目录,不可逆;删除前有二次确认。
  • 级联删除子代理 —— 删除父会话时连带清理其子代理会话(默认开启),行上显示 +N 子代理,确认框会写明将连带删除多少个。见下。
  • 安全护栏 —— 删除权限只看目标会话自身是否已归档:已归档 = 可以直接删(若还在运行,先中止它的当前回合);未归档 = 拒绝,并提示先去侧栏归档。见下。
  • 子代理会话 —— 默认不显示(与侧栏一致,见下),需要时切到「子代理」筛选查看,行上标出父会话 id。
host:   sessionManager 服务(facts / remove / removeMany / archive / unarchive)
        └─ 信任围栏 HTTP 路由 /session-manager/api/*(服务未被代理时的载体)
client: 设置页「会话管理」(settings.section)+ 删除确认弹窗

安装

从本地路径(开发时最常用):

dsh plugin --profile web add /绝对路径/dsh-session-manager
# 重启 dsh web,然后打开 设置 → 会话管理

从 GitHub 直接安装:

dsh plugin --profile web add git+https://github.com/wjackiedev/dsh-session-manager.git

卸载:

dsh plugin --profile web remove dsh-session-manager

本包在 package.json 里保留了 "private": true,避免误发布到 npm;从 git 或本地路径安装不受影响。

开发

没有构建步骤,也没有需要安装的依赖(@deepseek-ai/cordis 是 peer dependency,由 DSH 宿主提供):

git clone git@github.com:wjackiedev/dsh-session-manager.git
cd dsh-session-manager
npm test

测试是普通的 node 脚本,会打印 PASS / FAIL 行:

npm run test:host      # 只跑宿主策略测试
npm run test:client    # 只跑客户端 ghost 行测试

改动代码前请先读 AGENTS.md——它记录了架构、六条硬性不变量(fork 子会话必须存活、SESSION_GONE 拒绝恢复已删除会话等)以及代码与提交约定。

权限边界

  • 只读宿主已有的持久化状态:sessionPersistence.list()/stat()/locate()。
  • 写:workspaceRegistry.archiveSession() / setState()(归档集合),以及删除会话日志目录。
  • 不注册任何面向模型的工具,不写会话日志,不触碰会话之外的任何文件。
  • 客户端 bundle 只 require 冻结基座里的模块(react、@deepseek-ai/dsh-client-ui-primitives),没有非基座 external,也没有构建步骤。

级联删除子代理会话

删除一个会话时,默认连带删除它的子代理后代(整棵子树,深度不限)。三个要点:

  1. 判别条件是 origin === 'subagent',不是 parentSession。 SessionHeader.parentSession 同时也是 fork 血缘字段("the session this one was forked from")——fork 出来的分叉会话是普通顶层会话,有自己的工作区记账,用户从没要求删它。只按 parentSession 级联会把它们一起删掉。单测里专门锁了这条(FORK 子会话必须留下)。
  2. 深度优先 + 父会话最后删。 顺序是「最深的子代理 → … → 直接子代理 → 目标会话」,中途失败时留下的是仍在界面上的父会话,而不是一堆孤儿子代理。
  3. 全有或全无,但闸门只看目标会话自己。 判定是否可删只看目标会话是否已归档:已归档 → 整棵子树都可删;未归档 → 只要有成员仍在进程中活动,整批拒绝(错误码 SESSION_LIVE,回报具体哪些 id 忙),不会删一半。子代理不会被单独归档(它们在侧栏里根本不是可管理的行),所以闸门不能下放到子代理,否则"归档父会话"就永远删不掉了。

remove(id, { cascade: false }) 可以只删目标本身(HTTP 侧对应 { id, cascade: false });单删子代理会话时同样会对它的后代级联。删除完成后界面会用 host 返回的 childrenRemoved 汇报「已删除 N 个会话(含 M 个子代理)」。

已归档就能删,未归档不能删

规则的落点是目标会话自己的归档状态,不是它是否常驻内存:

目标 在进程中活动 结果
已归档 否 直接删
已归档 是(运行中 / 已打开) 可删:先 agent.cancel({ kind: 'disposed' }) 中止当前回合,再删文件
未归档 否 直接删
未归档 是 拒绝(SESSION_LIVE),提示先去侧栏 ⋯ 里归档
未归档 子代理在活动 拒绝(提示先归档目标会话)

为什么这么定:

  1. "归档"就是"我不用了"的显式声明。 harness 的归档是单向隐藏,用户归档之后唯一的诉求就是清理掉它;此时再以"还在内存里"为由拒绝,等于让归档变成一个无法收尾的操作。
  2. 归档会话本来就看不见。 我最初拒删的理由是"删了但侧栏还在显示"——而归档集已经把会话从所有分组视图里隐藏了,这条理由对归档会话不成立。
  3. 未归档的仍然拒删是有必要的:ctx.sessions 里常驻的会话即使文件被删,harness 的列表仍会继续返回它(ApiSessionList.list 对 live 会话优先于索引),于是侧栏里会出现一个点开是空壳的行。归档这一动作正好把这个残影藏起来。

技术上做不到的部分:AgentHandle.dispose() 是 capability,只有创建该 agent 的持有者能拆,ctx.agents.get(id) 只给裸 Agent,所以插件无法把会话从进程里真正卸载。能做到的是 cancel() + 有上限的 whenIdle()(保证之后不再往已 unlink 的 inode 写),进程内那条 pending 记录要等重启 dsh web 才消失——这也是为什么 facts() 要额外暴露 onDisk,由客户端用它把"文件已不在"的行立即从管理页隐去。删除成功后的提示会写明「已中止 N 个运行中的会话」。

删除后的残影与恢复闸门

删除只动磁盘,进程/客户端里可能还留着一份"残影",所以两个入口都要挡:

  1. 管理页不再列出残影。 客户端列表来自 harness 的会话 store,删除后它仍可能保留一条摘要(ApiSessionList.list 对常驻会话优先内存条目;客户端的 ordered-baseline 合并又会跨重连保留旧摘要)。此时 host 的 facts() 里已经没有这一行了——所以判定条件是「缺 fact 或 onDisk === false」,两者都从管理页隐去,顺带把这类 ghost id 从所有统计里剔除。页面还会对这类 ghost id 调用一次 sessions.refresh(),把侧栏里的残影也一并冲掉。
  2. 恢复必须日志仍在。 unarchive() 先 sessionPersistence.stat(id):返回快照 = 日志在磁盘上(或仍是会落盘的 pending create),允许恢复;undefined = 文件确实没了,直接拒绝(错误码 SESSION_GONE)。已删除的会话仍留在归档集合里——它不影响任何界面,反而正好把可能残留的客户端条目继续挡在侧栏之外。

「未归属工作区」是什么意思

就是侧栏里那个「未分组」组(group.ungrouped):该会话不在任何工作区记录的 sessionIds 记账名单里。插件行上会显示 (未归属工作区)。三种来源:

  1. 子代理会话 —— session.create({ workspaceId }) 是唯一的记账入口,而子代理由父 agent 直接派生、不走这条 RPC,所以永远不记账。插件默认隐藏它们(harness 侧栏同样隐藏:tree.ts 的 sessionVisible() 第一条就是 origin !== 'subagent'),切到「子代理」筛选才可见。
  2. 以 cwd 而不是工作区创建的会话 —— session.create 只接受 workspaceId 或 cwd 二选一,传 cwd 时不记账(headless / ACP / SDK / CLI 路径)。
  3. 工作区注册被删,或 cwd 失效 —— harness 的"删除工作区"只删注册、保留日志(其会话就落到未分组);而 WorkspaceEntity.sessionIds 只投影 canonical cwd 等于记录路径的会话,目录被删/改名/软链变化都会被过滤掉,host 启动日志里会出现 filtered session ... from membership 警告。

已知代价("不改 harness" 的边界)

  • 删除后 workspace 记录里的 sessionIds 会留一个死 id。它会被 header 索引过滤、不显示,但没有公开 API 可以摘掉(WorkspaceRegistry.requireTable() 是私有的)。
  • 投影缓存(<dsh-home>/storages/session_projcache/sessions/<id>.json)会残留一条;读取侧有 session identity 校验,永远不会被误用。
  • 上述两项不影响使用,只是磁盘上的留痕。

兼容性

  • 需要当前 sessionPersistence.list() 返回 { header, revision, sizeBytes } 快照的 DSH(插件内部做了 snapshot.header ?? snapshot 兼容读取)。
  • 依赖的宿主面:sessionPersistence、workspaceRegistry、sessions(agents 为可选查找)。
  • 宿主的 HTTP 路由注册面:webServer.register({ kind: 'prefix', path, handler })。

参与贡献

欢迎 issue 与 PR:

  • 提交前请阅读 CONTRIBUTING.md(开发环境、测试要求、提交信息规范)与 AGENTS.md(架构与硬性不变量)。
  • 架构或行为变更请同步更新 CHANGELOG.md 的 ## [Unreleased]。
  • 安全问题不要开公开 issue,请按 SECURITY.md 私密上报。
  • 本项目采用 Contributor Covenant 行为准则。

License

MIT © 2026 Jackie Wang (wjackiedev)

—/ 5

No ratings yet

Verified DSH bundle

Commit 7d381c69ee88

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout