Skip to main content
H

dsh-session-manager

he0119/dsh-session-manager

DSH 的会话管理插件:设置里的一页,用来导出/导入 .dshsess 会话包,以及把工作区与会话迁移到新目录——改写会话日志 cwd、迁移日志目录、重挂工作区归属,并可选择性搬迁会话创建的文件。

Install

dsh plugin --profile web add github:he0119/dsh-session-manager

README

dsh-session-manager

DSH 的会话管理插件:把工作区与会话搬到新目录,把会话导出 / 导入成 .dshsess 包,以及用 WebDAV 在多台机器之间同步会话。

中文 | English

在 DSH 里,「会话属于哪个工作区」不是可改字段,而是由会话日志 header 里的 cwd 推导出来的: 宿主既没有 move / reassign API,也不接受项目目录与 cwd 不一致的日志。所以手工搬目录,轻则会话变成 Ungrouped,重则加载时报 corrupt session log。本插件把「改 header cwd + 移动日志目录 + 让宿主当场认下新的归属」 三件事一起做,每次写盘前都会先摆出这份计划、事后能逐字节回滚。(为什么必须这样,见 .agents/notes/implemented/。)

同步走的是同一条路:远端只放 .dshsess 包,拉取时按配置里的映射把 cwd 改写成这台机器的路径, 所以两台机器的同一个项目处在不同目录也能互通。

它能做什么

入口适合什么
设置里的「会话管理」页日常使用:在「会话」分页里逐条归档或删除;用下拉框选来源(目录或「未分组」)做迁移;勾选导出 / 导入会话包;在「同步」分页里按 WebDAV 配置同步
5 个模型工具直接跟会话说「把这个工作区的会话搬到 ~/dev/xxx」或「跟远端同步一下」,由模型先预演再落盘

两个入口的迁移编排是同一份代码,所以弹窗里说会迁移几条,实做就是几条。

页面上的会写盘的动作(删除 / 迁移 / 导入 / 同步 / 回滚)都是一个按钮开一个确认弹窗:弹窗里摆的就是 那份只读计划(哪些会话、落到哪、备份落在哪、有什么问题),按「确认」才落盘,按「取消」则什么都不发生。 模型工具那一路仍然是 plan / apply 两次调用——模型自己先看清楚再落盘。

安装

从 npm 装(推荐):

# 用宿主自己的插件命令:它会替你建/填充 profile,并维护依赖与锁文件
npx @deepseek-ai/dsh@next plugin --profile desktop add @he0119/dsh-session-manager
# 然后重启 DSH

开发时装本目录(lib/ 不进 git,所以先构建):

pnpm install && pnpm run build
npx @deepseek-ai/dsh@next plugin --profile desktop add /path/to/dsh-session-manager
# 然后重启 DSH

装完重启 DSH,设置 的左侧导航里会多出一页「会话管理」(排在官方那几页之后)。 本包不支持 github: 形式的安装:仓库把构建放在 prepublishOnly、lib/ 不入库,git 装法拿不到产物。 宿主版本要 0.2.0-rc.1 及之后的 0.2.x(engines.dsh 与两个 DSH peer 都是 ^0.2.0-rc.1, @deepseek-ai/cordis 另写 ^4.0.4)。宿主按 peer 范围决定装不装:范围收不下运行中的那个版本, 整条 bundle 会被跳过(日志里是 skipping profile bundle … is incompatible with dsh …)。它判范围时带 includePrerelease,所以同一 minor 线上的新 rc(0.2.1-rc.1)照旧收得下;换 minor 线 (0.3.0-rc.1)时要跟着改这一行。

使用

设置 → 会话管理

会话——对着整个会话库逐条管理

  • 列表按目录分组,组头可折叠(「全部收起 / 全部展开」在列表上方),组头那一下就是 整组勾选——"把这个旧项目的会话都归档 / 都删掉"因此是一次点击。组头只写着这一组的名字:注册表里的 工作区标题,没加入工作区的目录就取项目身份(有 git remote 时用它的最后一段,如 dsh-session-manager; 没有才退回显示本机路径),所以行里不再重复归属那一列,省下的宽度留给标题;项目身份与本机路径都在 名字那一格的悬浮提示里(两行:身份在上、路径在下——同一个仓库在本机的两个克隆靠路径区分); 有项目身份的目录还在名字后面挂一枚主机名小标签(github.com、git.hehome.xyz),扫一眼就知道 哪些目录是认得出身份的仓库,标签自己的悬浮提示给全整条身份。 落在侧边栏「未分组」那一组里的会话在行上挂「未分组」小标签 (组头说的是目录,标签说的是侧边栏把它放在哪儿)。子智能体会话缩进到父会话的下一级(跟着它、排在 它后面;父会话在别的目录组里时留在自己组里缩进并标明父在哪儿),"这条属于谁"不必读标签猜。每一行 显示标题、字节数与创建时间;宿主侧边栏看不见的会话也在这里(那边 点不到它们),行上挂标签说明原因:「子智能体」/「空白」/「已归档」,还活在宿主内存里的那条挂「活动中」;
  • 筛选:一排小胶囊——子智能体 / 空白 / 已归档 / 未分组 / 活动中,各带库里的条数;勾几枚看几类 (多选=任一命中),「全部」一键清空;下面一行是标题 / id 搜索框("按 id 指名道姓"与"按标题 回忆上次那个问题"都常用,所以两个都搜)。筛过之后头部报「显示 N / M 条」,「全选」选的是眼下列 出来的这些(筛出空白再一键全选就是要删它们),勾选不会因为切换筛选而丢;
  • 归档 / 取消归档:勾选后一键收起或放回。走宿主自己的归档能力,即时生效——侧边栏马上跟着变, 不必重启;勾父会话会连它的子智能体一起归档(族是一个单位,理由见下面删除那节);宿主没提供这个 服务(非 Web profile)时按钮置灰并说明原因;
  • 删除:勾选 → 点「删除所选」,弹窗里列出会被删的每一条、日志数与备份位置 → 确认。删除先备份到本插件的 备份根,再删掉整个会话目录;删完侧边栏要等宿主重新扫描才会少掉那几行。还活在宿主内存里的会话 会被拒绝——先在宿主里关掉它。想反悔就去「备份与回滚」里点「恢复」;
  • 子智能体跟着父会话走:勾一条父会话,它的子智能体会话(连同更深的那些)会一起删——弹窗里逐条列出 并标明"随父会话删",一个备份装下整族(父日志一没,子会话在侧边栏里就再没有入口,留在盘上等于 变成看不见的残留)。子智能体不能单独删 / 单独归档 / 单独导出:它的勾选框是灰的(悬浮提示里写着 该勾哪条父会话),直接调接口也会被拒并指名父会话——只删它会在父会话日志里留一条指着不存在会话的 条目。父会话已经不在库里的孤儿例外:没有可跟随的会话,它只能单独收拾。分叉出来的会话不算: 它是自洽的一条普通会话(源会话的历史已经拷进它自己的日志),父删掉它照样能打开,所以不带走。

迁移——把一个目录的会话搬到另一个目录

  • 源与目标各是一个下拉框,框里的值就是要用的路径;候选=已是工作区的目录 + 库里真有会话的 目录(后者带会话条数)+「未分组」,不必凭记忆手输路径。候选之外的路径走「浏览…」或「手输路径」: 桌面端「浏览…」弹系统目录对话框,浏览器里展开页面内的目录浏览,宿主没提供选择器时这个按钮不出现。 有 git remote 的目录,那一行是项目身份 + 本机路径(github.com/he0119/dsh-session-manager — /home/uy_sun/dev/dsh-session-manager):下拉框里没有悬浮提示可用,路径不能从这儿消失——同一个仓库 在本机的两个克隆只能靠它区分;
  • 源还可以是「未分组」:侧边栏「未分组」那一组里、且有 cwd 的会话,可以横跨多个目录,一次 全部收进目标工作区——这是唯一一处能跨目录的来源,因为"这两个目录里那两条不属于任何工作区的会话"本来就是一件事。这个来源下"连同未分组的会话"与"搬迁会话产物"两个开关不适用(界面直接 说明,不摆成能点却没用的样子);
  • 目标目录必须已存在;另有可选的新建工作区标题、是否连带会话创建过的文件、是否连带未分组的会话;
  • 整个来源一起搬,或只挑其中几条:来源下的会话会列出来(同样显示标题,id 在悬浮提示里, 目录来源下在「未分组」那一组里的那几条带「未分组」标签),上面一个标题 / id 搜索框按关键词收窄,勾任意一条 即切到「只选其中几条」,一次只处理一个来源。这一页不给类别胶囊:候选本来就把侧边栏看不见的会话 排掉了,那几类在这里永远是 0,摆出来只会让人以为筛选坏了;
  • 候选与宿主侧边栏对齐:子智能体会话(挂在父会话下面)、空白会话(一个回合都没开始过)与已归档 会话都不在候选里——侧边栏里看不见的会话不该被"顺手搬走"。点名要迁一条这样的会话时,计划里会如实说 明它被隐藏了(子智能体那条还会告诉你该点名它的父会话);
  • 子智能体跟着父会话走:勾一条父会话,它的子智能体会话(连同更深的那些)一起搬(归档与导出同样: 勾父会话就把子智能体一起归档 / 一起装进包)——各自的日志 header 改写 cwd、各自的会话目录搬进目标项目目录,一个备份装整族(否则族会被拆到两个目录里,而 子智能体本身是搬不动的:它不在候选里,点名它又是向上的牵连)。分叉出来的会话不算(它自洽、 父搬走也照样能打开)。搬完成员资格不变:子智能体本来不算工作区成员,搬完还是不算;计划里会报「其中 N 条是 子智能体会话」;
  • 点「迁移」开弹窗,里面是:逐条列出会被搬走的会话(子智能体跟着走的那几条缩进一级、挂「随父迁」)、 会话数、日志数、字节数、源项目目录 → 目标项目目录、注册表会怎么变(新建还是复用目标工作区、 写进注册表几条、从哪些工作区搬出、是否移除已空的工作区)、产物计划与跳过原因;
  • 按「确认迁移」:改写每个日志 header 的 cwd(只动首帧,其余字节不变)→ 搬会话目录 → 写回注册表 → 交给宿主自己改(复用 / 新建目标工作区、把会话挂过去、摘空就删,见下面「注意事项」)→ 独立复核 (等价于宿主的 corrupt 判据,另加一条"会话 id 不变"的复核)→ 留下字节级备份;完成后页面会告诉你这次 改动是不是即时生效,或者需要重启 DSH。

备份与回滚——在迁移分页下方,列出本插件写过的每一份备份(时间、迁移还是删除、会话数、 源 → 目标);点「回滚」/「恢复」先在弹窗里摆出动作清单,确认才动。迁移的备份点「回滚」:把会话目录、日志字节与工作区注册表一起还原 (空掉的目标项目目录也会删掉,与迁移清理空源项目目录对称)。删除的备份点「恢复」:只把会话目录搬回 原位——删除从头到尾没碰过注册表。

传输——把会话带走,再带回来

  • 列表里的会话显示的是标题(鼠标悬浮才给出完整标题与 id):uuid 对人是零信息,认会话靠的是 「我上次问那个问题的会话」;读不到标题的老会话回落显示 id。导出列表与「会话」页照单全收、 连侧边栏不显示的会话(子智能体 / 空白 / 已归档)也列出来——"这条会话要不要带走 / 要不要删"是人的 决定,替你预先藏起来只会让"我明明有这条会话"变成找不着的谜;迁移则相反(见下);
  • 导出:勾选会话 → 浏览器下载一个 .dshsess 包。包里是这些会话所有代次日志的原始字节 (逐条带 sha256),不含会话创建过的普通文件。列表按目录分组(组名是工作区标题;没加入工作区的目录 拿这个目录的项目身份最后一段当组名——有 git remote 就是它,没有才退回显示路径,两种情况都标出来; 身份与本机路径都在组名那一格的悬浮提示里,认得出身份的还挂一枚主机名小标签), 组头那一下就是整组勾选 / 取消——「把这个工作区的会话都带走」因此是一次点击; 组头最前那个尖角是折叠(收起只把组内的行收起来,组头与"这组几条 / 勾了几条"都还在),列表上方那行 「按目录分组」后面是全部收起 / 全部展开:收起是显示上的事,"全选整库"照旧按列出来的那些算, 不会因为点了箭头就悄悄少导出几条; 两级一眼分得开:组头是带底色的横幅 + 文件夹图形,会话行缩进挂在它下面、挂对话气泡图形(会话标题 是用户写的一句话,很容易长得像个目录名,靠读字分不出来)。子智能体会话再往里缩一级、挂在它父会话 下面(列表里的父子与"删 / 搬父会话会带上谁"是同一棵树);侧边栏「未分组」那一组里的会话在这里 跟着目录走,行上挂一枚「未分组」小标签说明这件事。
  • 筛选:与「会话」页同一套——一排小胶囊(子智能体 / 空白 / 已归档 / 未分组 / 活动中,各带库里的条数, 多选=任一命中,「全部」一键清空)加一个标题 / id 搜索框;两者一起用时是"与"(搜 foo 且只看空白)。 筛过之后头部报「显示 N / M 条」,「全选整库」选的是眼下列出来的那些;筛空的那一组整组不画 (组头底下没有行,看着像坏了),组头报的也是筛剩下的条数;
  • 导入:选包 + 选目标工作区 → 点「导入」,弹窗里逐条列出会创建什么、cwd 会被改写成什么、哪些会被 跳过、注册表会怎么变 → 确认才落盘(全是跳过时确认按钮是灰的)。导出不问:它只打包下载,不改本机任何 东西。导入永不覆盖:库里已有同 id 的会话只跳过并报告;包里没有 cwd 的 会话落 _no-cwd 项目目录,也不进注册表。

说明——把名词与代价一次讲清:分类词典(可见 / 子智能体 / 空白 / 已归档 / 活动中 / 未分组,其中「未 分组」就是侧边栏那一组:不属于任何工作区、而且侧边栏会显示它)、每页做什么、数据从哪来(会话库与 注册表两条路径),以及常见疑问(「未分组」到底算哪些会话、删完侧边栏为什么还在、迁移什么时候要重启、 备份与「回滚」「恢复」的分工、子智能体跟着父会话走、包里有什么)。动作页(会话 / 迁移 / 传输 / 同步) 上只留"当下要做的决定",那些词条与边界条件都在这页上,所以那几页每段说明都不超过两行。

宿主没有 webServer 服务时(例如只用工具的前端)这一页不出现,工具照常可用。

同步(WebDAV)

多台机器上的同一个项目往往装在不同目录(/home/alice/dev/proj 与 /opt/work/proj),所以不能直接 同步会话库:库里的目录名与 header 的 cwd 绑死,换台机器就对不上。这一块的做法是把远端当中转—— 远端放的是 .dshsess 包,落地仍然走导入那条路,由它把 cwd 改写成这台机器的路径。

配置(插件配置里的 sync 块):

sync:
  url: https://dav.example.com/dsh        # WebDAV 地址(服务器根也行);插件在它下面自建 dsh-session-manager/
  machineId: robot-a                       # 可选,缺省取主机名;两台机器别用同一个 id
  username: alice                          # 可选(Basic)
  passwordRef: DSH_DAV_PASSWORD            # 可选:**凭据引用名**(环境变量名);留空就用 DSH_DAV_PASSWORD
  mapping:                                 # 显式映射:远端 cwd → 本机目录(两台机器各写各的)
    /home/alice/dev/proj: /opt/work/proj
  timeoutMs: 30000                         # 可选

这一节也能在界面里改:设置 →「会话管理」→「同步」分页(这张表单在卡片里),URL、机器名、账号、 密码、超时与映射表都能直接编辑(留空的字段按框里那份灰字生效:机器名留空就是主机名,超时留空就是 30000 毫秒),按「保存」写进 profile 那份配置文档 (~/.dsh/profiles/<name>/cordis.patch.yml)。sync 是活字段,改完不用重启就生效;三个路径字段 (sessionsRoot / registryPath / backupRoot)仍然只在配置文件里。路径映射在表单里逐行增删: 左边是远端记下的 cwd,右边是这台机器上的目录;远端那一栏必须逐字对上别的机器记下的路径,所以同步过 一次(弹窗里那份计划见过它们)之后它会把这些 cwd 列成候选,不用手抄。

密码在那张表单里直接填:那是个只写的输入框,按「保存」把密码写进宿主机凭据库 ($DSH_HOME/.credentials.yaml),配置文件里始终只有引用名——值不回显,界面只报「已配置 / 未配置」。 引用名不在这张表单上:它就是配置里的 sync.passwordRef(缺省 DSH_DAV_PASSWORD),要换名字(或者 本来就用环境变量)在插件配置页那份通用表单里改。启动环境里已有的同名变量会遮住写入,这时界面把密码 框标成不可改。

配完先点一次「测试连接」:它只读探一次远端(两次 PROPFIND,不改远端任何东西),回的是一句结论 ——连得上/认证过不过/地址对不对,认证失败还会分清是「没填用户名」「引用名里没有值」还是「服务器不认 这套用户名密码」。测的是已保存的配置(宿主的运行时按配置现读),所以有未保存的改动时按钮是禁用的。 写权限不在这里验:第一次同步自然会建那一层,测试不该往别人的服务器上放东西。

多机共用一份配置:远端索引里每条会话还记着项目身份——仓库的 git remote(规范化成 host/owner/repo)与会话 cwd 在仓库根之下的相对路径。拉取的时候先用本机的候选目录(会话的 cwd + 注册表里记着的工作区路径)认一遍身份,认出同一个仓库就落 本机仓库根 + 相对路径。于是几台机器可以 用字面一样的配置(url 相同、machineId 缺省取主机名、mapping 留空),同一个仓库克隆在 /home/alice/dev/proj 与 /opt/work/proj 也照样对得上;反过来,两台机器上同名的目录若是两个仓库, 不会被认成同一个项目。没装 git、不是仓库、没有 remote、本机没这个项目——一律退回下面那张映射表, 计划里会点名是哪个仓库没认出来。

mapping 与身份共存:显式映射优先(配了就按配的落),身份是自动那条路。所以非 git 目录、 或者想强制落到别处,仍然靠映射。

远端布局:插件在 url 下面自建一层自己的命名空间,dsh-session-manager/<machineId>/index.json 是 这台机器贡献了哪些会话,dsh-session-manager/<machineId>/<id>.dshsess 是一条会话一个包——所以 url 可以直接填 WebDAV 的服务器根或账号根,不必自己写这一层。一机一格:WebDAV 没有锁,每台机器只写 自己那一格、读别人的全部,就不会互相盖掉。

在「同步」分页:点「同步」先在弹窗里算一份只读计划(读远端,什么都不写),报出「会拉取 N 条 / 会推送 M 条」,三张表(会拉取 / 会推送 / 两边都有、这次不动)各自按项目目录分组——组头是这一组的名字(工 作区标题,没加入工作区就是项目名)+ 一枚主机名小标签 + 这一组几条,项目身份与本机路径在悬浮提示里;行里只 剩动作、会话名与大小,会话名后面跟着与会话列表同一套的类型标签(子智能体 / 空白 / 已归档 / 活着 的,本机已有的那几条才挂得上)。「这次不动」那张表的第三列是哪台机器持有的另一份。按「确认同步」才真 的拉取与推送;落地那一段弹窗正文换成进度条与「正在推送 12 / 84」,下面是当前那一条的标题——分母是这 一段真正会做的条数(跳过的那些不算),拉取与推送各自走一遍。规则与边界:

  • 两边都有同一个 id 时,按内容与「最后活动时间」择新:内容逐代一致(cwd 之外)就不动;本机确实 是远端那份的前缀就重推刷新远端;远端领先(本机这份是它的前缀)或两边各自写过而远端更晚,就先备份 本机那份、再换成它(旧的那份随时能从「迁移」页的备份清单里恢复);有一边读不到最后活动时间 (老索引 / 宿主没挂投影缓存)或者两边一样新,就两条都不动,报告里说清是「远端更新」还是「两边各自 写过」。「最后活动时间」取宿主从日志内容里折出来的两枚钟里晚的那枚(最后一次提问、最后一条消息 ——agent 自己写进去的也算,所以"推送之后本机又跑了几轮"照样分得出高下),复制、改写 cwd 都不影响 它。判据是代次指纹, 与 cwd 无关——落地一定会改写别人的 cwd(库目录名与 header 的 cwd 绑死),按字节比会把拉取来的 那份判成"两边各自写过",连"同步下来继续写"都无法再推送回远端;
  • 本机严格领先时重推:两边共有的代次内容一致(cwd 之外逐条一致),说明远端那份确实是本机这份的 前缀,刷新它不会丢任何代次;同一 id 有多台贡献时,拉取的一侧取领先的那份而不是格子名排前面的 那份;
  • 空白会话不参与同步:建出来但一个回合都没聊过的会话不推送(正文里报一句「跳过 N 条空白会话」), 下一次推送还会把它从自己那格索引里撤下来——别的机器因此不再看得到它;本机这条是空白而远端有内容时 以远端为准(空的那份没什么可保护的);
  • 删掉的会话下一次推送就从自己那份索引里消失(远端那份包不主动删),别的机器已取走的副本不受影响;
  • 没有 cwd 的会话不硬塞路径,按 _no-cwd 落地(与导入同一条口径);没配映射的 cwd 会被跳过并列出来;
  • 拉取来的会话要宿主重新扫描才会出现在侧边栏:改完注册表之后插件把活儿交给宿主自己做(与迁移同一套 语义),宿主上没有那套动作时才提示重启。

同步也要跑 plan 那一套:工具 sync_sessions 默认只预演,apply:true 才落盘。

模型工具

工具写盘说明
plan_session_migration否只读计划:会话/文件数、目标项目目录、注册表变更、阻塞问题
migrate_sessions需 apply:true默认 dry-run;执行前做字节级备份,事后自动复核
rollback_session_migration是按备份目录字节级回滚
verify_workspace_sessions否复核某项目目录内日志与 header 的一致性
sync_sessions需 apply:true与 WebDAV 远端同步(拉取别人推送的、推送本机独有的;两边都有同 id 时按内容与最后活动时间择新);默认 dry-run

注意事项

  • 改完注册表交给宿主自己做,正常不必重启:磁盘上的 workspace.json 只是第三份拷贝——宿主内存里那份 才是权威(写入才改它,绝不从磁盘重读),它另有一份启动时建的 header 索引。所以插件改完注册表之后不是 "等宿主自己发现",而是把活儿交给宿主自己做:先让它按磁盘重看一眼(刷新 header 缓存与"这条会话住哪儿" 的索引),再复用 / 新建目标工作区、把会话挂过去、把源侧摘空、删掉已空的工作区。这几件事宿主自己会落盘、 也会通知界面,所以侧边栏立刻按新归属显示——不动进程,也不杀正在跑的回合。会话 id 全程只被挂靠与摘除, 一个都没变;已存在的工作区也是复用自己那条记录,id 不变。 宿主上没有那套动作时(只有工具没有界面的前端、老版本),工具与页面都会如实说 需要重启 DSH,并且 重启前别改任何工作区——新建 / 改名 / 归档都会用内存副本把这处改动覆盖掉。
  • 先看不写:工具与页面上的计划(弹窗里那份)都不写任何字节;每次真正写盘前都会先留一份字节级备份。
  • 同步会按「谁更新」择新:远端那份更新(或两边各自写过而它更晚)时会先备份本机那份、再换成 它;有一边读不到最后活动时间就两条都不动(见「同步」那一节的边界)。
  • 只动首帧:改写只重新压缩 header 那一帧,其余 frame 字节原样保留,回滚可逐字节还原 (连同可选搬迁的会话产物)。
  • 路径:会话 cwd 与注册表路径都不带结尾斜杠;projectKey 会把 /、\、: 折叠成 -, 极少数路径会因此编码碰撞,这种情况计划层会直接拦下。
  • 插件配置:sessionsRoot / registryPath / backupRoot 三个可选字段可覆盖上述默认路径; sync 块配 WebDAV 同步(url / machineId / username / passwordRef / mapping / timeoutMs)。 密码只放引用(环境变量名),配置文件里没有明文:界面上直接填的密码进的是宿主机凭据库 ($DSH_HOME/.credentials.yaml),不是这份配置。

文档

  • .agents/notes/——每条决策的依据与被放弃的做法(中文):多帧 zstd 的静默 丢数据、启动不变式、有损编码的碰撞、.dshsess 容器的取舍、生效模式、同步怎么判「谁更新」、 空白会话为什么不搬,以及界面上的那些取舍
  • docs/internals.md——决策地图:按主题索引到上面那些笔记
  • docs/development.md——本地流程:依赖、构建、测试、开发实例
  • docs/releasing.md——发版流程

许可证

MIT

Related plugins