dsh-session-inherit
Verifieddsh-session-inherit · v0.5.5 · MIT · Web UI
Inherit a long DeepSeek Harness session into a clean one: adds an "Inherit" row to the session "..." menu, mechanically extracts concrete anchors (task, recent instructions, files edited, recent tool failures, commands, todos) from the source session log
Install
dsh plugin add dsh-session-inherit Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
dsh-session-inherit(继承)
把一段跑了很久的会话,交接给一个干净的新会话。
在会话行的「…」菜单里多出一行 继承。点它 → 预览一份从源会话机械提取出来的 交接单 → 确认 → 新建一个不带旧历史的会话,把这份交接单作为它的第一条消息发出, 工作在新会话里继续。原会话原样保留。
/inherit 命令是同一个入口(指令提示里能搜到):
置顶 ← DSH 原版(Pin)
重命名 ← DSH 原版(Rename)
分叉 ← DSH 原版(Fork)
继承 ← 本插件(order 350)
归档 ← DSH 原版(Archive)
除「继承」外的四行都是 DSH 原版自带的条目(sidebar.workspaces.session.menu.item
slot 的官方内容)。如果装了别的会话菜单插件,你的列表里还会多出它们的条目——
本插件只负责「继承」这一行。
为什么不是 /compact、也不是「分叉」
现在的 DSH 里已经有两条"继续干"的路,但这个需求是第三条:
| 做法 | 它搬过去的东西 | 对本场景的问题 |
|---|---|---|
/compact |
把历史换成摘要,会话不变 | 实测把 541,227 token 压到 25,602,4 步之内退化又犯了,重复词还从「做。」变成了「好。」——可见既不是长度的问题,也不是上下文内容的问题 |
| 官方分叉 | 复制源会话的整段历史前缀 | 等于把退化诱因一起带走,新会话一睁眼就是几十万 token 的历史 |
| 本插件的继承 | 只搬可核对的具体锚点,历史一律不带 | 新会话的上下文从几千字符起步 |
为什么不用 LLM 做摘要
/compact 那次的证据很直接:摘要留下的是"要做验收、要写报告"这类抽象意图,
丢掉的恰恰是文件路径、下一步参数这类具体锚点。模型攥着几句模糊意图,在抽象层反复
自我告诫,而每次告诫的句式几乎一模一样——这本身就是在喂重复退化。
所以本插件的交接单一次模型调用都不发,只做机械提取:任务原文、最近的用户指令 (原话,不改写)、动过的文件、跑过的命令、最后一次待办状态、源会话最后一条回复。 每条都能追回源会话去核对。
交接单开头还写死三条执行规约:
不要复述本单,不要总结本单,不要回应"收到/了解了"。 读完直接做「下一步」。第一步只做一个具体动作,做完停下汇报;不要同时铺开多个方向。
交接单长什么样
交接单正文的语言跟随界面语言(客户端在 preview/commit 请求里带 lang):中文界面
生成中文交接单,英文界面生成英文交接单——不然英文用户拿到的正文,他自己和模型都读不顺。
下面这份是中文输出。
📋 会话继承单
> 来源:`session-xxx`(标题) · 共 15 轮 · 最后活动 2026-10-08 19:37
> **执行规则**:不要复述本单…
## 任务目标 ← 源会话第一条真实用户消息
## 改过的文件(实际写入/修改过) ← 来自 edit/write 的 file_path,按最近改动排序
## 最近失败 ← 源会话最后几次工具报错(带轮次与错误码)
## 最近的用户指令(由旧到新) ← source.kind === 'user' 的原话
## 读过的文件(只读取过,未修改)
## 文中提到的路径 ← 工具参数正文里扫出来的,可能是噪音
## 项目内相对引用 ← 基准目录未确定,需要时用文件搜索确认
## 最近执行的命令
## 待办(源会话最后一次 todo 状态)
## 源会话最后一次回复
## 下一步 ← 你在预览框里写
章节标题不带序号。序号是逐节写死的字面量,会随"某一节有没有内容"漂移—— 早先版本里客户端提示写死了「## 7. 下一步」而实际生成的是「## 8.」,用户照着找 永远找不到。
「最近的用户指令」只认 source.kind === 'user'。实测某个真实会话的 42 条
user/message 里,有 27 条是 agent-instructions / runtime-context /
skill-catalog / time-context / 记忆提示 / 模型选择 / 压缩检查点 这类运行时注入
——全是噪音,现在被挡在门外。
两个高价值锚点
改过的文件只取 edit / write 这类写入工具的结构化 file_path。实测某真实
会话:写入类调用 85 次、读取类 58 次,改过 17 个文件、读过 43 个——两类混在
一起时,读过的文件会把真正改过的挤出上限。write 的 content、edit 的
new_string 正文里出现的路径只算「文中提到」。
最近失败来自 tool/result 的 isError。实测某真实会话 374 条 tool/result
里 11 条失败,形如:
- (t5) `edit` FS_STALE_VERSION:cannot write "…\inject-codex.ps1": file changed since it was read — re-read the file, then retry
每条都带轮次——同一节里的失败可能横跨好几轮,不标轮次的话新会话分不清哪些是
陈年旧账。这是"接着干什么"最直接的线索,而且纯机械可提取。成功的 363 条、以及
用户主动打断(AbortError)和自动审查被中断这类噪音会被丢掉;同一条报错在多个
工具上重复出现时按文本合并。
绝对路径锚点还会做一次存在性检查,已被删除或改名的会标上 (已不存在)——省掉 新会话去翻一个不存在的文件。
用法
入口一:会话行菜单
- 鼠标移到侧边栏任一会话,点行尾的「…」;
对哪个会话点都行,包括已经冷掉的会话(读取走
sessionQuery.readSession,不激活 agent)。 - 点 继承。
- 弹出预览,从上到下是:源会话信息(标题 / 轮次 / 改过几个文件 / 失败几条 / 命令数 / 待办数) → 「下一步」输入框(可以不写)→「创建后立即开始工作」勾选框 →「继承当前对话的 预设」勾选框 → 一行折叠着的交接单。
- 在「下一步」里写要它做的事——越具体越好(点名哪个文件、做哪一个动作), 然后点「创建并继承」。要改交接单全文时,点「▸ 展开交接单全文(可编辑)」。 交接单默认折叠是因为多数时候你只想写一句话,不想逐字审阅几千字。
新会话会自动打开,标题是 继承: <源标题>,工作目录、模型路由、agent preset 默认
都跟源会话一致;不想继承预设就在预览框里取消那个勾,新会话改用 DSH 默认预设。
勾选框「创建后立即开始工作」默认勾上:交接单会经
sessionController.prompt 作为一条正常用户消息发出并立刻开始。
取消勾选时改为直接写入会话日志(session:append),只记录、不触发首轮,
打开后你自己说要做什么。
「下一步」输入框和交接单是两个独立来源:宿主收到 commit 时,如果两者都在, 会把输入框的内容写进那份交接单的「## 下一步」一节——所以即使你展开改过交接单, 也不会有哪一边被悄悄丢掉。
降级会明说。 如果宿主没能自动开始(prompt 失败或不可用),对话框不会静默
关掉,而是停在原地显示原因和新会话 id,并给一个「打开新会话」按钮——以前这里只
写 console.warn,界面静默消失,用户会以为新会话已经在跑了。
重复继承会被提醒。 源会话(常常是冷的)改不了标题:
sessionTitle.rename(session, title) 的契约要求传入活着的 session 对象(内部断言
ctx.sessions.get(id) === session),而 sessions.get 的文档写明"冷会话返回
undefined"。所以"这个会话被继承到哪儿去了"记在
%DSH_HOME%\session-inherit\history.json,在你再次点开预览框时提示
「这个会话已经被继承过 N 次,最近一次 …→ …」——正好是你准备再次继承的那一刻。
记录只存会话 id 和时间戳,最多保留 200 个源会话、每个 10 条。
入口二:/inherit 命令
/inherit 继承当前会话
/inherit <sessionId> 继承指定会话
/inherit --next 先跑一次 npm test 继承当前会话,并指定第一步
/inherit <sessionId> --next 换个文件继续 两者一起用
不带 --next 的裸文本只可能是会话 id,不合法就如实报错,不会被猜成"下一步"。
入口三:session_inherit 工具
注册给 agent 的同名工具,参数 sessionId(必填)、nextStep、start。
用途是让 agent 自己在上下文变长时提议换会话,或让用户通过 agent 表达意图。
三条入口最终走同一条链路:读源会话 → 机械提取 → 新建会话 → 挂工作区 → 注入交接单。
回查源会话:session_read 工具
交接单首行写着源会话编号,但新会话没法直接读它:会话日志是
%DSH_HOME%\sessions\<工作区转义>\session-<id>\session.vN.jsonl.zstd,多帧 zstd 拼接
(实测 2.7MB 文件切成 1067 帧,Node 的 zstdDecompressSync 只解第一帧就停)。
所以本插件另外注册了一个只读工具补上这一环:
session_read { sessionId, mode: 'search' | 'summary' | 'tail', query?, limit? }
search(默认):按关键词搜源会话,返回命中处前后各一段上下文summary:结构化摘要(任务目标 / 改过的文件 / 最近失败 / 待办 / 最后一次回复)tail:最后 N 个有内容的事件——看"上次到底停在哪"
新会话遇到"交接单里那个文件是干什么的"这类问题时可以直接回查,不用你回去翻。 它只读,不创建也不修改任何东西。
「附上原对话编号、让新对话顺着编号去找」这个想法的闭环就在这儿:光有编号没用, 得让它读得到。
安装
从 npm 装(推荐,一条命令):
dsh plugin --profile <profile> add dsh-session-inherit
也可以不经过 npm,直接从仓库装。改 profile 的 package.json,两处都要加:
{
"dependencies": {
"dsh-session-inherit": "link:/path/to/dsh-session-inherit"
},
"dsh": {
"profile": {
"bundles": [
"dsh-session-inherit"
]
}
}
}
bundles 才是加载权威——只写进 dependencies 不会挂载。装完重启 DSH。
桌面端的 profile 由 Electron 独占管理,全局 CLI 会拒绝操作它; 那种情况直接改该 profile 的
package.json再重启即可。
版本适配
在 DSH 0.2.0-rc.2(Windows)上验证通过。插件依赖若干 DSH 内部服务与菜单
slot 名,DSH 升级后可能失配。如果菜单项不出现、或插件页显示「异常」,先查
profile 的 compatibility.json——profile 启动时会校验插件对 @deepseek-ai/dsh
与 dsh-* 的 peerDependencies,不满足的 bundle 会被整体跳过(日志里打印
skipping profile bundle)。把当前 DSH 版本加进 compatibility.json 的允许
列表即可。
组件构成
- 宿主半
index.mjs:注册POST /__session-inherit/preview与POST /__session-inherit/commit(同源校验,仅本机 loopback 使用) - 客户端半
client.js:注册sidebar.workspaces.session.menu.item(idsession-inherit, order 350) 与shell.overlay上的预览对话框
只消费公开服务:agents(硬依赖)、sessionQuery、sessionController、
workspaceRegistry、sessionTitle、agentPresets、agentDefaultModel、webServer。
除 agents 外全部按需查找,缺一个就降级一项,不会让整个插件失活。
配置
profile 的 cordis.patch.yml 里可以覆盖(全部可选):
- id: session-inherit
config:
enabled: true # false = 完全不注册
recentUsers: 6 # 交接单里带多少条最近的用户指令(1–50)
startByDefault: true # 预览框里那个勾选框的默认值
cacheTtlMs: 120000 # preview → commit 之间复用一次日志读;0 = 不缓存
已知限制(如实说)
- 它不治退化本身。 退化是在生成当前这一步犯的,属于解码层;本插件做的只是换一个
不带旧上下文的干净会话接着干,把"长历史 + 抽象意图"这两个诱因拿掉。新会话第一步
照样可能退化——那是解码层的事,交给在生成侧检测重复并中止的独立插件
(如
dsh-degen-guard),本插件不碰。 - 交接单是机械提取,不会替你想"下一步"。空着的话模型会先问你一句,这也是故意的。
- 源会话的第一条用户消息如果是很短的寒暄/引言,它就会以原样出现在「任务目标」里。
- "最后一段含空格"的路径要靠边界识别。 路径解析分三条路,按可靠性排序:
① 工具的结构化参数(
read/grep的path)整串就是路径,最可靠; ② 被引号/反引号包起来的整段,边界明确;③ 剩下的靠正则扫,而正则的末段不允许 含空白(否则会把后面的句子一起吞掉)。所以路径直接嵌在中文句子里、后面紧跟 说明文字时(X:\work\My Project Name 这个项目),会被 截断成X:\work\My Project。实测某真实会话里这类残留约 4 处,都在「文中提到的 路径」这一节(标题已注明"可能是噪音")。 - 同一行里用空格相连的两个路径会被当成一个——已用"中间段不允许出现盘符冒号"
挡住
copy X:\p\a.cs X:\p\b.cs这种;中文标点(,。;:、!?()【】《》) 也作为分隔符处理。 - 交接单上限 14,000 字符。超长时按优先级整节丢弃(先丢「文中提到的路径」→ 「读过的文件」→「项目内相对引用」→「命令」→「最后一次回复」→「待办」→ 「最近的用户指令」→「改过的文件」),「下一步 / 任务目标 / 改过的文件 / 最近失败」永不丢弃。实测一个 1976 事件的长会话生成 4,425 字符,只占上限 32%, 基本不会触发。
- 各节上限按信息密度定,不是拍脑袋。 在同一份真实会话上量过:交接单 6,235 字符里 「最近执行的命令」独占 23.9%,而它的内容是 180 字符处硬切的 PowerShell 长脚本碎片 ——读不全,也重跑不了;「读过的文件」+「文中提到的路径」再占 26.8%。收紧后是 改过 16 / 读过 8 / 提到 6 / 相对引用 6 / 命令 5 条×120 字符 / 待办 12 / 失败 5 条×200 字符, 核心四节只收紧长度、不砍条数。同一份会话从 6,235 降到 4,425 字符(−29%)。 交接单是新会话的首条消息、每轮都会重发,省下来的每一段都是持续成本。
- 源会话做过上下文压缩(
compaction/*)时,交接单会标注一句——压缩之后更早的 原文已经不在日志里,机械提取自然也就取不到。 - 源会话用的自定义预设默认就会带过去。 有些预设(比如第三方角色扮演类)不改会话
header 的
agentPreset(那里可能一直是standard),真正生效的预设记在宿主的%DSH_HOME%\.agent-presets\session-bindings.json里,形如{"session-xxx": "preset-xxxxxxxx"}。对话框里有「继承当前对话的预设」勾选框, 默认勾选:勾着的时候插件会把这个 preset id 当作新会话的agentPreset在创建时 传进去,新会话一开场就是那个预设;取消勾选则新会话用默认预设。交接单页脚如实写明 结果——预设:preset-… → 新会话已继承该预设,或「本次未继承」「挂载失败」。 - 真正完成继承的不是某个预设插件的 HTTP 接口。 比如
dsh-tavern的POST /api/tavern/bind-preset只写它自己的绑定账本,源码注释写明「真正切换 agent 预设请用聊天顶部选择器」。插件仍然会调它,目的是让账本保持一致;真正的继承走的是 宿主的agentPresets服务、在会话创建时完成。上一版把这两件事搞反了,于是出现 「交接单页脚写着绑定了预设、新会话却没有人格」。
测试
node test/extract.test.mjs # 提取器纯函数(32)
node test/host.test.mjs # 宿主半:假 ctx 跑通端点注册、preview/commit 全链路、/inherit 参数解析、预设继承、继承记录(18)
node test/client.test.mjs # 客户端半:模块协议、slot 注册、降级菜单行,以及菜单→preview→折叠/展开→确认→commit 全链路(5)
发布
每次发版要同时传两个附件:
dsh-session-inherit-<版本>.tgz—— 留档dsh-session-inherit.tgz—— 固定名
固定名那个是给 releases/latest/download/dsh-session-inherit.tgz 用的。latest 指向
最新那个 Release,附件名不带版本,市场里写好的安装命令才不会随发版失效——少传它,
那个 URL 下次发版就是 404。
npm test # 1) 改完代码,跑测试
# 2) bump 版本 → 提交推送 → 打 tag(脚本会校验 tag 与 package.json 的版本一致)
node scripts/release.mjs # 3) 打包 + 建 Release + 传两个附件
--dry-run 只列包内容、不打包不上传;--tag v1.2.3 显式指定 tag。脚本不碰 git——
提交和打 tag 仍由你手动做,它只管打包与上传。
与已有插件的关系
写之前查过现成的方案,没有完全重合的:
- WeiYe6/dsh-session-handoff(
dsh-session-handoff) 最接近:/handoff命令,同工作区新建会话 + 注入交接文档 + 自动打开。差别是它 用 LLM 总结最近 N 轮,而且只能在当前会话里敲命令;本插件的入口在会话行菜单, 对任意(含冷)会话可用,且不调模型、只搬具体锚点。 - ZhijiangTang/dsh-handoff:
确定性地导出一份
HANDOFF.md,但不建新会话。 - liangmianya/dsh-synapse: 可视化会话地图,按 DSH 原生 fork 关系连线——走的是"带上全部历史"的那条路。
@michengai/dsh-archive-manager:它还往同一个菜单里注册了delete-session(order 500)。本插件用 order 350,两者不冲突。
License
MIT