Skip to content

dsh-baize-session

Verified

dsh-baize-session · v0.1.2 · MIT · Web UI

Pick messages out of DeepSeek Harness sessions and relocate them into another session's opening context (Baize).

Install

dsh plugin add dsh-baize-session

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Readme

dsh-baize-session

English | 简体中文

npm version license

English: dsh-baize-session (Baize) is a dsh plugin that moves context between conversations, and manages the conversations of a workspace (archive / restore / delete / relocate).

dsh 插件,围绕对话做两件事:

  • 在对话之间搬运上下文——在你正待着的会话里挑出若干条消息,带到另一个会话:已存在的,或另一个工作区里全新开的。
  • 管理工作区里的对话——在一个标签页里列出全部(含已归档),归档、还原、真正删除,以及迁移到其他工作区。

名字取自白泽——传说中通晓万物的神兽。dsh-baize-rules 注入的是要求,这个插件搬运的是上下文。

dsh web UI 里的「整理」标签页——目标工作区与目标对话两个下拉、显示已选条数与 token 估算的操作条,以及带 USER / ASSISTANT / CONTEXT / TOOL 徽章的消息表格

  • 挑中的消息会被改写成文本,合成一条 user/message,来源标记为 source.kind='plugin'、plugin='baize-session'——而不是重放事件。任意挑选的历史无法重放:会话存储要求 seed 必须从 seq 0 连续,一段不完整的历史不是合法前缀。
  • 注入块的结尾是 当前工作区是 <路径>。请在此基础上继续。——明确告诉模型这段引用的历史来自别的目录,全程不做任何路径改写。
  • 没有选中内容就不写入:篮子为空时直接拒绝,绝不会注入一个空壳。
  • 所有破坏性操作都有闸门:会话必须先归档才能删除,而归档的会话根本不能迁移。

功能

功能 说明
就在当前会话里挑 「整理」面板把本会话的消息列成勾选表格。点行看完整正文,点复选框才选中
随处收集 每条助手回复旁有 + 按钮,不离开聊天流就能收进来。篮子按会话独立存放
两种落点 在选定工作区里新建对话(内容作为该会话的 seed),或追加到当前已打开的会话
天然跨工作区 新建会话时以目标工作区为 cwd,因此它落在那个工作区的会话目录下——跨工作区搬运不需要任何路径改写
用名字而不是 id 选择器显示会话自己的标题(日志里的 session/title),没有标题时退回首行正文;原始 id 放在该行的详情里
按来源标注角色 user / assistant / context / tool,配色取自官方「轨迹」视图的 kind tag。context 这一档很关键:人类输入和系统注入在 dsh 里是同一个 user/message 事件类型
Token 预算 写入前先估算;超过 maxInjectTokens 会带着实测数字拒绝,而不是悄悄截断
工作区管理 「工作区」面板列出当前工作区的全部对话(含已归档),每行可归档 / 还原 / 迁移 / 删除,另有多选批量操作
打开中的对话也能迁移 还在内存里的会话可以迁移:先 flush,再在协调器的每-id 锁内改写工件,并让后续写入指向新位置
只把真实对话作为目标 「整理」面板的目标选择器会过滤掉空会话与已归档会话——与官方侧边栏的 sessionVisible 规则一致
一套运行时,两个入口 面板与 /baize-session 命令驱动同一套运行时,两者不可能出现分歧
自己不留任何数据文件 插件不在磁盘上保存自己的状态;它改动的东西(会话工件、工作区注册表)都属于 dsh

安装

dsh 插件通过 npm 分发,用 dsh plugin 装进某个 profile。

# 从 npm 装进 web profile(请使用实际已发布的版本号)
dsh plugin --profile web add [email protected]
pm2 restart dsh          # 用 pm2 托管 dsh 时执行
dsh --profile web

peer 依赖(@deepseek-ai/*)由 dsh profile 提供;若有缺失,pnpm 会依照 profile 目录下的 peerDependencies 解析。插件同时声明了 dsh.bundle(通过 cordis.patch.yml 把自己挂进插件树)与 dsh.client(浏览器端),两半都靠它才会加载。

卸载

dsh plugin --profile web remove dsh-baize-session
pm2 restart dsh          # 用 pm2 托管 dsh 时执行

如果 profile 的 dsh.profile.bundles 里还残留这一行,从 $DSH_HOME/profiles/web/package.json 里删掉它再重启一次。除此之外不会留下任何东西:本插件不写自己的数据(见数据位置)。

不打扰正在运行的 dsh 试装

装进一个单独的 profile,当前运行的 dsh 完全不受影响:

dsh plugin --profile smoke add [email protected]
dsh --profile smoke --dump-config   # 只组装并打印配置,不启动 dsh

本地开发(link)

// $DSH_HOME/profiles/web/package.json
"dependencies": {
  "dsh-baize-session": "link:/home/abc/work/plugin/dsh-baize-session"
}

然后在 profile 目录里 pnpm install,并把 dsh-baize-session 加进 dsh.profile.bundles。


快速上手

打开任意会话,点聊天视图旁边的 整理 标签页。这一个标签页里有两个面板,用标题行上的子标签切换:

[对话] [轨迹] [规则] [整理]          ← 顶层标签栏(本插件只占「整理」)
──────────────────────────────────
 整理 | 工作区                       ← 两个面板

整理 —— 把消息搬进另一个对话

① 目标工作区  [ /home/abc/work/plugin        ▾ ]
② 目标对话    [ 新建对话                     ▾ ]
─────────────────────────────────────────────────────────────
   已选 0 段                              [ 清空 ] [ 迁移 ]
─────────────────────────────────────────────────────────────
   12  USER         消息正文的第一行……                         ← 点行看全文
   13  CONTEXT      Current runtime context. This snapshot…
   14  ASSISTANT    回复正文的第一行……
  1. 选工作区(当前工作区默认选中并排在最前;末项可以手填绝对路径)。
  2. 选目标对话——新建对话,或该工作区下的任意真实对话。
  3. 勾选要带走的条目。点行只是看全文,不会选中它。
  4. 点迁移。

点开的消息行:完整正文展开在该行下方,带来源徽章、#seq 与字数;真正用于选中它的是行上的复选框

工作区 —— 管理这个工作区里的对话

「工作区」面板:顶部是工作区路径与「全部 / 未归档 / 已归档」筛选及各自条数,下面是批量操作条,表格行上并列显示类型徽章与状态徽章,行尾是该行的归档与迁移按钮

工作区:/home/abc/current/test        [ 全部 7 ] [ 未归档 6 ] [ 已归档 1 ]
─────────────────────────────────────────────────────────────────────────────
已选 0 个                              [ 归档 ] [ 还原 ] [ 迁移 ] [ 删除 ]
─────────────────────────────────────────────────────────────────────────────
☑ 问候与自我介绍     已对话  未打开   09-15 17:58 · 24 条消息   [ 归档 ] [ 迁移 ]
☑ 测试              已对话  进行中   09-20 12:42 · 31 条消息   [ 归档 ] [ 迁移 ]
☐ 已归档的对话       已对话  已归档   09-20 12:42 · 6 条消息    [ 还原 ] [ 删除 ]
操作 位置 规则
归档 行内按钮,或批量条 任意对话,打开或关闭的都可以
还原 行内按钮,或批量条 仅限已归档的对话
迁移到其他工作区 行内按钮,或批量条 不能是已归档的;打开中的对话也可以迁移
删除(连同磁盘日志一起删) 行内按钮,或批量条 仅限已归档,且需要二次点击确认

点击某一行(按钮与复选框以外的任意位置)会展开它的详情行:完整 session id、创建时间、消息数、工作区路径,以及当前是否打开。

同样的操作也可以纯键盘完成:

/baize-session                     # 等同 info:本会话 id、工作区、消息与篮子计数
/baize-session take 12 14          # 按 seq 收集(从当前会话)
/baize-session list                # 按工作区分组列出所有真实对话
/baize-session to /home/abc/work/led   # 在那里新建一个会话并写入篮子
/baize-session add session-1a2b…   # 或追加到一个已打开的会话
/baize-session drop                # 清空篮子

命令

子命令 语法 用途
info /baize-session [info|status] 显示本会话 id、工作区、消息/篮子计数,以及最近 listLimit 条消息及其 seq
take /baize-session take <seq…> 按 seq 从当前会话收集消息
list /baize-session list 按工作区分组列出真实对话(空会话与已归档只在末尾以计数提示,不列出),带名称与 id
drop /baize-session [drop|clear] 清空篮子
to /baize-session [to|move] <绝对路径> 在该工作区新建会话,并把篮子写入作为开场上下文
add /baize-session [add|append] <会话id> 把篮子追加进一个已存在的会话——该会话必须处于打开(在内存中)状态

对话的类型与状态

「工作区」面板每一行都带两个彼此独立的徽章,因为它们回答的是不同问题:

列 取值 回答的问题 判据
类型 空对话 / 已对话 / 子代理 这里面有对话了吗? blank——从没记录过 turn/start
状态 进行中 / 已打开 / 未打开 / 已归档 它现在在哪? 挂着 agent / 在内存里 / 已落盘 / 在归档集合里

它们不是互相替代的关系。空对话也可能处于进行中——会话一被创建 agent 就挂上了,而第一条 prompt 还没发;有内容的对话也可能处于未打开。把两者折进一个徽章,说清了一件事就丢掉了另一件。

状态列里 已归档 优先于其它值(侧边栏隐藏会话的依据就是它);类型列里 子代理 优先(子代理会话不能迁移)。


面板

两个面板都在本插件占用的同一个 conversation.view 标签页里(id baize-session-tidy,order 40)——纯增量,不替换任何官方组件。+ 按钮注册在 conversation.chat.assistant-actions(id baize-session-collect,order 20)。

共同行为:

操作 结果
点一行 展开/收起该行的完整正文(整理)或详情行(工作区)。两者都不会选中它
点复选框 选中/取消选中;详情保持原样
操作条 常驻,两个面板共用同一套 .baize-bar。未选中任何内容时,按钮是禁用而不是隐藏
下拉 自绘浮层(原生 <select> 与 dsw 主题不搭);点外部或按 Esc 关闭

「整理」面板的特点:内容列表始终是当前对话;消息表格随窗口高度伸缩(没有固定高度)。

「工作区」面板的特点:标题+筛选行与子标签行都是 32px,因此没有任何一行比邻居低;类型列与状态列各固定 76px、时间列固定 150px,所以它们纵向对齐,不会随标签长短漂移。

消息行按来源打标:

徽章 含义 配色来源(官方「轨迹」视图)
USER 人类输入(source.kind === 'user') .user —— state-business-primary / state-business-tertiary
ASSISTANT 模型回复 .assistantVioletBright
CONTEXT 由插件或 harness 注入——运行时快照、<system-reminder>、记忆库注入,以及本插件自己写入的整理块 .contextGreen
TOOL 工具结果(tool/result 事件) .toolAmber

user/message 不等于"用户说过这句话":dsh 把人类输入与注入的上下文记成同一个事件类型,只靠 data.source.kind 区分。抽样一个真实会话,其分布是 {user: 4, plugin: 10}——那次对话里大部分内容根本没人打过字,这正是要给两者分别打标的原因。


注入的内容

每次迁移写入一条 user/message,用 createUserMessage() 构造,并以 surface 事件必需的标记 { surfaceOp: 'append' } 发布:

以下是本会话开始前,我(用户)从其它会话整理过来的上下文,供你参考:

【来自会话 06094031 / 工作区 /home/abc/work/plugin】
user: token 预算是在哪强制的?
assistant: 在 relocate.ts 里,写入之前——它是抛错而不是截断。

---
当前工作区是 /home/abc/work/led。请在此基础上继续。
  • 每条都保留来源标注(user: / assistant: / context: / tool:),让接收方模型能分清哪些是提问、哪些是注入。
  • 内容按来源会话分组,并保持收集顺序。
  • 目标是已存在的会话时,这段内容作为普通消息追加;目标是新会话时,同一条消息成为该会话的 seed。之所以用 seed,是因为 dsh 按 checkpoint 落盘:一个没人说过话的全新会话,走 append 是永远落不到磁盘的。
  • 新会话随后会被挂到它所属的工作区(workspace.attachSession),这正是它能出现在侧边栏的原因。

哪些会列、哪些不会

两个面板的过滤规则故意不同:

整理 —— 目标选择器 工作区 —— 表格
空会话(从没跑过 turn) 隐藏 列出,类型 空对话
已归档 隐藏 列出,状态 已归档(除非你用筛选排除)
子代理子会话 隐藏 列出,类型 子代理
其它 列出,显示名字 列出,显示名字

「整理」的选择器回答的是"这段内容能落到哪",所以只给出合理的目标;「工作区」的表格回答的是"这里有什么",所以全部列出并允许你操作。两者取自同一份会话列表,因此不可能互相矛盾。


操作会拒绝的情况

操作 何时被拒 提示
迁移进已有会话 目标不在内存里(未打开) 目标会话不在内存中,无法追加(先在侧边栏打开它)。
迁移 篮子为空,或估算超过 maxInjectTokens 拒绝信息里带实测大小
迁移 目标路径是相对路径 目标路径必须是绝对路径:…
归档 / 还原 当前 dsh 的注册表没有暴露写入接口(版本过旧) 当前 dsh 版本未暴露…接口
删除 会话不是已归档 只能删除已归档的对话:请先归档,再删除。
删除 请求缺少 confirm: true 删除需要显式确认(confirm: true)。
迁移 会话已归档 已归档的对话不能迁移:请先还原它。
迁移 会话是子代理子会话 子代理会话不支持跨工作区迁移。
迁移 定位不到持久化工件 当前持久化后端不支持定位会话工件,无法跨工作区迁移。
迁移 没有任何已知信息能把后续写入指向新工件 当前运行时既不暴露 live 写入器、也不暴露持久化写入状态…

迁移的实际步骤(顺序不能乱)——会话通过 cwd 归属于某个工作区,而注册表拒绝把「存储 cwd 与工作区路径不一致」的会话挂上去,所以先改工件、后换记账:

  1. 若会话打开中,先把缓冲事件 flush 到当前工件。
  2. 在持久化协调器的每-id 锁内(后端提供锁时)读取工件,只改写头行的 cwd(保留 version),写到该 cwd 推导出的路径——经临时文件 + 原子重命名,原始文件先"停靠"起来,以便失败时能把字节放回去。
  3. 同步搬走内存里的指针:live 写入状态(meta.cwd)、内存中的 session header、注册表的 header/path 索引——若记账交换失败,它们一起回滚。
  4. 从旧工作区 detach,attach 到新工作区,然后删掉已清空的旧目录。
  5. 任一步失败,原始字节回到原始路径。这一点比听起来更重要:dsh 启动时会校验"会话位置必须与 header 的 cwd 一致",不一致会让它拒绝启动整棵插件树。

配置(Config)

字段 类型 默认值 含义
maxInjectTokens number,必填 8000,由本包的 cordis.patch.yml 设置 估算超过这么多 token 就拒绝写入。拒绝信息里带上实测大小,所以"选太多"是可见的,而不是被悄悄裁掉
listLimit number,可选 15,代码内默认(patch 并未设置) /baize-session info 列出最近多少条消息

包里实际发布的 patch 就是下面这样——要覆盖 listLimit,加在同一个 config 块里即可:

# cordis.patch.yml
- insert:
    - id: baize-session
      name: 'dsh-baize-session'
      config:
        maxInjectTokens: 8000

HTTP API

浏览器端无法读取会话、创建会话,也无法触碰工作区注册表,因此面板与宿主 web 服务器上的 /baize-session.api 通信。它委托给命令所用的同一套运行时。

请求 Body / 查询 返回
GET /baize-session.api ?sessionId=<id>(必填);&source=<id> 列出另一个会话的消息;&view=workspace 改为返回工作区面板 PanelState:{ sessionId, cwd, messages, basket, projects, sessions, maxInjectTokens };带 view=workspace 时返回 WorkspacePanel:{ workspace?, workspaces, sessions }
POST /baize-session.api { sessionId, op: 'take', seqs?, messageIds?, sourceId? } { ok, added, missing, state }
{ sessionId, op: 'untake', seq?, messageId?, sourceId? } { ok, removed, state }
{ sessionId, op: 'drop' } { ok, state }
{ sessionId, op: 'estimate', target } { ok, tokens }
{ sessionId, op: 'relocate', target } { ok, sessionId, mode, tokens, sources, state }
{ sessionId, op: 'archive' | 'restore', targetId | targetIds } { ok, results, panel }
{ sessionId, op: 'deleteSession', targetIds, confirm: true } { ok, results, panel }
{ sessionId, op: 'moveSession', targetIds, toWorkspaceId } { ok, results, panel }
  • target 为 { kind: 'new', cwd } 或 { kind: 'existing', sessionId }。
  • 工作区相关的 op 里,targetId(单行)与 targetIds(多选)可以互换。
  • 批量结果逐条回报(results: [{ id, ok, error? }]),所以一条被拒不会中断其余;信封上的 ok: false 表示每一条都失败了。每个工作区 op 都连着返回刷新后的面板,因此界面每个动作只需一次往返。
  • 用户能自行处理的拒绝(相对路径、空篮子、追加目标未打开、超出预算、未归档、缺少确认、迁移已归档)以 400 返回,消息可直接展示给用户;其余为 500。

数据位置

本插件不保存自己的任何状态——没有配置文件、没有缓存、没有索引。篮子只活在进程内存里,重启即空;这正是"为这一次搬运而选"的应有范围。

它改动的东西属于 dsh:

路径 读取 写入
$DSH_HOME/sessions/<转义后的 cwd>/<会话 id>/session.jsonl.zstd 会话日志——已关闭的用 inspect 读回,既不提交恢复也不发布 仅由迁移(改写 header 的 cwd 并移动文件)与删除(移除)写入
$DSH_HOME/storages/workspace.json 工作区记录、注册表全局的会话归档集合 仅由归档 / 还原 / 迁移写入

其它位置一律不碰,卸载后也不留残留。


模块结构

src/core.ts       纯逻辑:事件读取与作者分类、消息列表、renderInjection、会话命名、工件编码/改写辅助
src/relocate.ts   搬运运行时:篮子、面板状态、take/untake/drop/estimate/relocate、工作区与持久化访问
src/workspace.ts  工作区运行时:对话列表、归档/还原、删除(agent 收尾、live store detach、工件移除)、跨工作区迁移(改写 header、原子换名、记账交换)
src/api.ts        宿主 HTTP API:/baize-session.api 的 GET 状态(两个面板)+ POST 操作分发
src/index.ts      apply:两套运行时 + /baize-session 命令 + API 挂载(inject: commands/sessions/tokenMeter/webServer/sessionPersistence)
lib/client.js     浏览器端,手写产物:「整理」视图及其两个面板 + 「+」槽位,window.__ModuleLoader__.load({ id, factory })
test/core.spec.ts        纯函数的单元测试(fixture 照抄真机日志形状)
test/workspace.spec.ts   迁移所依赖的工件编解码测试(zstd 帧布局、头行改写、往返一致)
test/client.smoke.mjs    用 react-test-renderer 跑真实客户端产物并断言其行为
cordis.patch.yml  挂载元数据(插入 baize-session 插件行与默认配置)

对外入口(见 package.json 的 exports):.(index)、./client、./src/*、./package.json。


开发与即时反馈

pnpm test            # vitest 单元测试,随后跑客户端冒烟测试
pnpm test:client     # 只跑客户端冒烟测试(node test/client.smoke.mjs)
pnpm test:watch      # 保存即重跑单元测试
pnpm build           # tsc -p tsconfig.build.json → lib/
pnpm typecheck       # tsc --noEmit
改的是 位置 怎么看到效果
宿主(src/*.ts) 经 pnpm build 产出 lib/*.js 构建后重启 dsh(宿主加载的是 lib/)
浏览器(lib/client.js) 手写,无构建步骤 刷新页面即可——产物从磁盘按内容哈希版本号直接提供

lib/client.js 是手写的:官方的 clientBundle tsdown 预设没有发布,所以 dsh 仓库之外的插件要自己写 window.__ModuleLoader__.load({ id, factory }) 这层壳,只把种子模块(react、ui-primitives)留作外部依赖。它没有任何类型检查或打包环节,因此 test/client.smoke.mjs 会带着打桩后的宿主 API 用 react-test-renderer 加载真实产物,断言两个面板的版式、交互顺序、徽章与它们的 CSS 规则。

破坏性路径怎么测:删除与迁移会改写持久状态,因此它们只在专门造的测试对话上演练,且必须在隔离的 dsh 实例里——绝不拿任何人在意的会话试。迁移的验证方式还包括:迁移后继续往该会话写入,确认消息落在新路径。


发布

# 1. 升级 package.json 里的版本号,并同步两个 README 的安装示例
# 2. 本地验证
pnpm build && pnpm typecheck && pnpm test
# 3. 提交、打标签、推送
git add -A && git commit -m "release: vX.Y.Z"
git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin main && git push origin vX.Y.Z
# 4. 发布——真正出货的一步
npm publish --access public

凭据。 npm publish 使用你自己的 npm 凭据(~/.npmrc 或环境变量)。绝不要提交 token,也不要把它粘进 README 或 CI 日志。


许可

MIT