@heixui/dsh-session-suite
v0.10.1
Published
Session checkpoints for DSH: rewind a conversation with its files and memory, permanent session deletion, and shareable exports
Maintainers
Readme
dsh-session-suite(会话套件)
为 DeepSeek Harness(DSH)提供会话检查点能力:把对话 连同工作区文件和会话记忆一起回滚到任意一条消息,永久删除会话,把会话导出成图片或单文件网页, 并且维护一套跨会话、跨工作区的长期记忆。
这是一个 DSH 插件包(Host + Web Client),对标 Trae 的会话操作体验,已在 desktop profile 上验证加载。
| 能力 | 位置 |
|---|---|
| 回滚到某条消息(对话 + 文件 + 记忆) | 每条最终助手消息的 ↺ 按钮,输入框上方的行内确认条 |
| 删除某一轮(只动对话与记忆) | ↺ 旁边的 🗑 按钮 |
| 删除选中的一段对话 | 会话 … 菜单 → 删除对话片段(可选轮次) |
| 永久删除会话 | 会话 … 菜单 → 删除任务(二次确认弹窗) |
| 分享选中的片段 | 会话 … 菜单 → 分享会话:勾选轮次后生成图片 / 网页 / Markdown / JSON / 复制文本 |
| 快照管理 | 会话 … 菜单 → 会话快照,以及 设置 → 会话快照 |
| 长期记忆(全局 + 工作区双库) | 设置 → 长期记忆;【记住:…】;memory 工具 |
| 用 WorkBuddy / Trae 积分跑模型 | 不在本包内 —— 见 用 WorkBuddy 和 Trae 的积分 |
长期记忆
两个隔离的库(global 与按工作区划分的 workspace)、四因子重要性评分(频率 · 新近度 · 情感强度 · 独特性)、
动态衰减加冷归档、相似聚类并压缩成摘要、以及从重复标签里提炼规则型记忆。
最关键的一点:隐式记忆不会偷偷入库。每轮结束后由一次轻量模型调用提出候选,进入设置页的待确认队列,
只有你点「保留」才真正写入;而 【记住:…】 / 【新增全局记忆:…】 这类显式指令会跳过队列直接写入。
评分公式、阈值、生命周期、指令、存储结构都在 docs/long-term-memory.md。
各操作分别动到什么
| 操作 | 对话 | 会话记忆 | 工作区文件 | |---|---|---|---| | 回滚(↺) | 截断到该消息 | 裁剪到该处 | 恢复到该轮快照 | | 删除一轮(🗑) | 提问 + 回答 + 中间的步骤一起移除 | 裁剪到该处 | 不动 | | 删除对话片段 | 从选中的第一轮(含它的提问)截断 | 裁剪到该处 | 不动 | | 删除任务 | 从磁盘移除 | 移除 | 不动(文件夹保留) |
只有回滚会把文件写回去,其它删除都不碰工作区。会话日志是只追加的,所以「截断」只能是「保留前缀 + 原来的会话退休」——选中范围之后的内容会一起消失。
分享
分享弹窗会列出每一轮对话(你的提问 + 回答预览),默认全选,取消勾选即可排除。然后选输出形式:
- 图片:把选中轮次画成一张 PNG(用 canvas 直接画,不依赖任何第三方库);
- 网页文件:单文件 HTML,内联样式、跟随明暗主题、可直接打印成 PDF;
- Markdown / JSON:贴进 issue 或喂给别的工具;
- 复制文本:纯文本进剪贴板。
两种富格式都会真正渲染 Markdown,而不是原样吐出:标题、粗体/斜体、行内代码、代码块、有序/无序列表、引用、管道表格、链接。Host 侧把日志解析成带内联样式的 block(lib/markdown.js),HTML 渲染器和 canvas 渲染器吃的是同一份结构,所以网页和图片不会出现两种排版。
截图会跟着一起导出:附件不在会话日志里,Host 通过 attachments.imageHostPath 取到原文件,按预算内联成 data URL,网页因此仍是单文件。图片里截图按整卡宽度绘制(限高、保持比例);网页里每张截图是一个按钮,点开用内置查看器按原始分辨率看——页面只带一小段内联脚本,不发任何外部请求。
另外要说清楚:位图(PNG/JPG)里的图片不可能点开。需要「点开看原图」就分享网页文件,而不是导出的 PNG。
导出到底包含什么
会话日志里大部分不是对话:运行环境、技能目录、时间采样、子代理回执、工具调用,以及模型在工具之间写的过渡性说明。这些都不该进导出:
- 只有
source.kind === "user"的消息才算「你说了什么」,user 角色下的其它事件全部丢弃; - 默认勾选 只导出最终回答:每轮保留你的提问 + 最后那条答复;
- 取消勾选则导出每个助手片段,包括工具调用之间的过程说明;
- 富格式里永远不出现工具输出,Markdown / JSON 也走同一套取舍。
同一轮里的多个助手片段会合并成一张卡片,只有工具调用、没有正文的步骤直接丢弃,不会导出成一堆空标签。
真正的「在线链接」需要一台服务器托管内容;这个包用可离线打开的网页文件来替代,而不是生成一个只有本机能打开的链接。
安装
从 npm 安装:
dsh plugin --profile desktop install @heixui/dsh-session-suite发布用的是带作用域的包名,因为不带作用域的
dsh-session-suite在 npm 上属于另一个项目 (chc-1986/dsh-session-suite),不是这个。作用域只是 npm 上的包身份——插件 id、存储目录、 bundle 名仍然是dsh-session-suite,所以旧版本留下的数据照样能找到。
从本仓库检出目录安装:
# 从本仓库检出目录安装
dsh plugin --profile desktop install .或者让运行中的 Harness 安装(plugin_manager 就是这样做的):
plugin_manager({ action: 'install_bundle', target: 'D:/path/to/dsh-session-suite' })安装后刷新页面即可。整个套件只有一个 Host 行(session-suite),配置写在 cordis.patch.yml;Client 部分由
package.json 的 dsh.client 字段自动发现。
回滚是怎么实现的
DSH 的会话日志是只追加的,而且是唯一事实来源,所以回滚不能删除事件。套件把它做成一次原子的三段式操作:
- 保护:先给工作区和记忆账本拍一个临时 undo 快照(10 分钟有效)。
- 对话:
sessionController.fork({ sessionId, atSeq })把事件前缀完整分叉成新会话(边界包含该消息、 沿用同一个cwd与 agent preset)。 - 文件:恢复该轮开始时记录的快照——改过的文件写回、轮次里新建的文件删除、会话开始前就存在的文件不动。
- 记忆:裁掉晚于目标序列的记忆条目。
- 退休:把原会话归档(侧边栏隐藏,日志保留),整个过程不销毁任何数据。
「撤销回滚」会把这五项全部还原:文件、记忆、被归档的会话,以及分叉出来的子会话。
快照在磁盘上
%DSH_HOME%/storages/dsh-session-suite/
sessions/<sessionId>/index.json # 检查点索引
sessions/<sessionId>/manifests/*.json # 路径 -> {hash, size, mtime}
sessions/<sessionId>/blobs/<ab>/<hash> # gzip 压缩的文件内容,按内容寻址
memory/<sessionId>.json # 会话记忆账本
memory/_global.json # 跨会话长期记忆
undo/<id>.json # 回滚撤销包- 增量:manifest 只记录路径与哈希,相同内容只存一份;未变化的文件靠上一份 manifest 的
(size, mtime)指纹直接跳过,不再读取。 - 配额:每个会话保留最近 10 个自动检查点;锁定的一律保留;临时撤销包不占配额,10 分钟后清理。
- 忽略清单:
node_modules、.git、缓存、构建产物、日志、锁文件、二进制文件都不记录;config.ignore可追加自定义规则。 - 上限:单文件超过 2 MiB、单次超过 4000 个文件会跳过,避免拖慢对话。
配置
- insert:
- id: session-suite
name: dsh-session-suite
config:
keep: 10 # 每个会话保留的自动检查点数量
autoCheckpoint: true # 每轮开始时自动拍快照
maxFileBytes: 2097152
maxFiles: 4000
sweepIntervalMs: 300000
ignore: [] # 追加的忽略规则目录结构
index.js Host 半部:注册路由、按轮拍快照、清理定时器
client.js Client 半部:菜单行、消息操作、确认条、弹窗、设置页
lib/api.js 受保护的 Fetch 路由(POST api/session-suite)
lib/service.js 会话操作:rollback / undo / delete / share / deleteMessage
lib/checkpoints.js 内容寻址的增量快照存储
lib/memory.js 会话记忆账本(支持按序列裁剪)
lib/share.js 会话日志 → Markdown / 文本 / JSON
locale/{en,zh}.json 插件管理器的展示信息与文案
tools/ 自测脚本与预清理脚本(不随插件打包)
docs/ 需求与 API 笔记0.4.0 改了什么
本次重构的是记忆引擎,会话相关功能未变。
| 方面 | 改前 | 改后 |
|---|---|---|
| 重复记忆 | 46 条里同一主题占 7 条 | 维护幂等(连续 6 轮验证);存量重复已合并进冷库 |
| 常驻层 | 2 条扁平记忆,混在「相关」区块里 | 最多 5 张主题卡片,独立分区标注 |
| 情景记忆 | 没有 | kind: 'episode',带发生时间、情境、结果;衰减加倍 |
| dense 重排 | 实现了但默认关,权重未验证 | 可选,默认关;权重经实测校准(8 条改写查询 MRR 0.535→0.760,recall 不变) |
| 隐私分级 | 只有 scope | public/internal/secret;凭据自动识别;secret 永不注入、永不导出 |
| 隐式队列 | 关键词闸门误杀 7/10 合法候选 | 闸门已移除;空队列会说明原因 |
embedding 是可选增强,默认不下载任何东西。 不装模型就是纯词法检索,功能完整;装了才有跨语言与改写召回。
实测效果与安装方式见 docs/memory-architecture.md 和 tools/download-embedding.mjs。
0.4.1 改了什么
新增侧边栏入口,修掉几个「看起来能用、实际没生效」的问题,并把界面上说不清的数字说清楚。
| 方面 | 改前 | 改后 | |---|---|---| | 入口位置 | 只能在 设置 → 会话快照 / 长期记忆 里找,三步才能到 | 左侧栏「会话套件」图标,点开即用;设置页那三个重复入口已移除 | | 记忆总数 | 面板显示 17,设置页显示 51 —— 同一句话两个数 | 面板遍历所有已登记工作区;每个数字都写明统计口径,并给出分工作区明细 | | 归档恢复 | 点了没反应,磁盘上的标记纹丝不动 | 修了两个 Host 服务参数形状不一致的问题,恢复真正生效 | | 快照数量 | 预算 10 张,列表却有 16 张,无从解释 | 区分「自有 10 张」与「继承 6 张」,继承项打徽章并说明为何不计入预算 | | 隐式提取模型 | 只能选「关闭」和「用当前模型」,看不到自己配的模型 | 从 Host 的 llm 服务枚举真实可用模型;默认关闭,不选就不消耗 token | | 长列表 | 配置区被五十条记忆挤到看不见 | 列表独立滚动,配置区始终可见 | | 会话记忆 | 一个从未写入的残缺区块,旁边就是 dsh 自带的轨迹 | 已移除 —— 轨迹是权威且不可改,手写笔记只会误导 |
关于记忆总数为什么会有多个数字:长期记忆分「全局库」和「各工作区的库」。
longterm.list 不带 cwd 时只返回全局库,带上才是全局 + 该工作区。面板没有工作区概念,
于是只显示了全局那部分。现在面板会遍历所有已登记工作区,并在界面上直接列出每个库的条数。
测试
全部自测脚本都在临时目录里跑,不会碰真实的 DSH 数据。
node tools/selftest.mjs # 快照 / 忽略规则 / 恢复 / 淘汰 / 记忆
node tools/selftest-memory-dedupe.mjs # 维护幂等,重复不再生成
node tools/selftest-resident-cards.mjs # 常驻卡片分组与预算
node tools/selftest-episode-memory.mjs # 情景记忆类型与衰减
node tools/selftest-memory-privacy.mjs # 隐私分级与导出过滤
node tools/selftest-implicit-queue.mjs # 隐式候选不再被误杀
node tools/selftest-memory-conflicts.mjs # 矛盾对只入队,绝不自动裁决
node tools/selftest-pain-distill.mjs # 失败轮自动提炼教训,去重且封顶
node tools/selftest-p2p3-wiring.mjs # 两个功能每一层都真的接上了
node tools/probe-dense-rerank.mjs # dense 重排的实测对比(需要模型)0.10.1 改了什么
0.10.0 上线后对着真实记忆库审计发现的五个缺陷,全部有实测证据。
| 方面 | 之前 | 现在 |
|---|---|---|
| 跨 store 矛盾 | 扫描在「每个 store 的循环内部」,所以全局规则与工作区规则互相矛盾时,两条都注入同一个提示词、且永不入队(实测:相似度 0.737,两侧都注入,队列为空) | 扫描额外把全局库与各工作区库对比;裁决时把输的一方写回它自己所在的 store |
| 队列读取 | conflicts(cwd) 用工作区过滤共享队列文件后把过滤结果写回,于是「不是我的」被当成「已失效」——在任一工作区打开记忆页,就删掉其他所有工作区待裁决的矛盾(实测 1 → 0) | 只清理本次真正检查过的条目,其他工作区的原样写回 |
| 教训里带凭据 | 失败信息里回显 sk-… 会让整条提炼出的教训被标 secret,而 secret 永不注入——教训(防止该错误的知识)消失了,凭据却留在库里 | 捕获时脱敏成 [已脱敏],教训保持可注入 |
| 解析不到工作区 | remember({scope:'workspace'}) 没有 cwd 会抛错,hook 捕获后只记日志,教训静默丢失 | 回退到全局库——一条到处都适用的教训本就该放那里 |
| 每轮开销 | 痛苦轮 hook 在每次 turn/end 重读并重解析整个会话日志:4 万事件实测 JSON 解析 186ms,而检测只要 1.15ms | 只读最后一轮窗口,开销降低 94% |
另有两个小的:自查纠错线索仍会误伤散文(「实际上全部生效了…算错了」);反转裁决后旧赢家同时带 supersededBy 和 frozen,让一条已退役的记忆看起来像被用户钉住的。
0.10.0 改了什么
两个功能出自同一个毛病:记忆库对错误的东西很自信。
| 方面 | 之前 | 现在 | |---|---|---| | 互相矛盾的记忆 | 被纠正的旧观念原样留在库里,和新观念一起注入,模型只能按新旧掷硬币 | 成对检测后入队,由人来选哪条为真;输的那条标 superseded,赢的冻结 | | 纠错被吞 | 长记忆只改一个从句,相似度 0.88 就会走「重复」分支:旧说法被强化,新说法被静默丢弃 | 反极性的近重复不再被吞,两边都留在库里交给队列裁决 | | 失败的轮次 | 一轮搞砸了什么都不会留下——模型当时正在救火,最不会写教训 | 从该轮自己的事件日志自动提炼教训,按失败主题去重,每库封顶 12 条 | | 聚类 | 矛盾对会被合并成一句「必须…;绝不…」 | 待裁决的矛盾对不聚类;教训也不聚类 | | 密钥误标(P2a) | 光出现「密钥 / token / secret」就算凭据,实测 8 条误标全部是误判,被静默封存 | 只认凭据实体(前缀、分隔符、17 位号、卡号、手机号、邮箱) |
为什么矛盾不自动裁决。 「同主题、反极性」同样能匹配:被撤回的旧观念、加了条件的规则、计划与实情。检测器分不清这三种,猜错就等于悄悄改写用户的信念。所以检测自动,裁决留给人。
立场是按从句比的,不是按整条。 真实记忆有 100–300 字、包含多个从句;实测本库 64 条活跃记忆里有 29 条同时含「要求」和「否定」,所以「这条是不是在禁止」对它们没有答案。第一版遇到这种就放弃判断,45 个人造矛盾里漏掉 20 个。改成按句号/分号切句、逐句比对后,改一个从句就能被发现,其余部分不必变。
为什么教训提炼不调模型。 本套件试过七次「每轮后台调一次小模型」,一次都没触发过,其中一次直接把 Host 搞崩;而且它对每一轮都要花钱,包括没出问题的那些。改成读该轮自己的事件日志:不调模型、不联网、不花 token,这才让它有资格每轮都跑。
已知边界
- 回滚不改写日志,而是「分叉到目标消息 + 归档原会话」。这是只追加日志支持的唯一形态,也正是撤销能够成立的原因。
- 快照覆盖的是文件,不覆盖外部副作用:数据库状态、运行中的进程、网络调用都不在其中。
- 跨会话记忆是本套件自己的账本(
memory/),不是 DSH 内核能力;因为由套件拥有,所以能一起回滚。 - HTTP 路由受 DSH Connection 保护(Host/Origin 校验 + 浏览器会话),只有已鉴权的 Web GUI 能访问。
许可证
MIT。
