dsh-compact-manager
上下文压缩管理。一个插件同时提供独立压缩模型和本地机械折页,合并自
dsh-compact-saviour 0.2.8 与 dsh-ledger-compact 0.13.4。
为什么要合并
两个插件都在包装同一个 @deepseek-ai/dsh-compaction-basic 引擎实例:
- saviour 用
Object.defineProperty覆盖compactRegion/compactIfNeeded/compactNow,把每条压缩路径改走自己的多段模型摘要器。 - ledger 包装
engine.summarize,让机械折页能在那里短路,并用「临时还原原型compactNow」的办法绕过 saviour 的包装器。
两个包装器无法共存:后装的会盖住先装的,这正是 ledger 需要那个绕过 hack 的原因。
合并后由同一个 Controller 拥有三个入口,按调用选策略,绕过逻辑不再需要。
压缩策略
页面上这三个控件描述的是同一件事的三个层次,优先级从上到下:
- 使能非模型压缩(
replaceDefault)— 最高优先级。开启后自动路径不再调模型。 - 启用自动压缩辅助(
enabled)— 插件是否接管自动压缩。关闭时第 3 项不被读取。 - 压缩方式(
mode)— 只在辅助开启时生效。
| 设置 | 自动压力/溢出压缩 | 手动按钮、/compact、/fast-compact |
|---|---|---|
replaceDefault 开启 |
机械折页 | 机械折页 |
enabled: false |
官方摘要器(mode 不被读取) |
按 engine |
mode: direct |
备用模型 1,失败切备用模型 2 | 按 engine |
mode: rescue |
官方,连续失败 rescueAfterFailures 次后备用模型 1,再失败切备用模型 2 |
按 engine |
engine: ledger |
— | 机械折页 |
engine: saviour |
— | 备用模型 1,失败切备用模型 2 |
engine: auto |
— | 配了模型就用模型,否则折页 |
enabled 和 mode 的四种组合只有三种行为。 关闭时 mode 完全不参与判定,
所以「关 + 官方优先」和「关 + 直接使用备用模型」效果相同。页面用三处表达这一点:
开关的提示先判断自身状态、关闭时下拉框置灰、下拉框提示写明「此项不生效」。
命令行的 ledger / saviour 覆盖上述一切,包括 replaceDefault——否则一个卡住的
配置就没有出口。
双模型回退
mode: direct 时自动压缩直接走备用模型 1;mode: rescue 时先让官方摘要器尝试
rescueAfterFailures 次,仍失败才交给备用模型 1。两条路径上,备用模型 1 失败都会自动换成
备用模型 2(若已配置)。
回退发生在 summarize 回调内部,不是包在 compactSurfaceRegion 外面。这个位置
是刻意的:压缩事务每次尝试都会写一组标记和一个 surface op,包在外面会让备用模型 1 的失败
留下需要清理的标记对,而部分提交根本无法重试。放在回调内部,事务只看到一次摘要调用,
补上的是哪一个模型产出的文本。
三条边界:
- 用户取消不换模型。 取消是你要求的,静默再花一次请求是反的。
- 只配一半的备用模型不生效。 有 provider 没 model 会在请求时
resolveModelInfo失败, 而那时代码已经用掉了备用模型 1 的尝试。保存时直接拒绝。 - 与备用模型 1 相同的备用模型会被去重。 同一个模型再试一次只会得到同样的错误。
路由租约(routes.js)激活时禁用回退:租约的存在就是要把这次压缩锁定在一个
provider/model 上计价,换模型会破坏它。
连通性测试
模型 tab 底部有两个按钮,分别发一次真实压缩请求测试备用模型 1 和备用模型 2,显示耗时。
能解析模型、能列出 reasoning 档位,都不证明它真的会回话。配对了 provider/model 依然
可能失败于:余额不足、密钥被撤、限流、模型拒绝自己的 reasoning 参数、或返回的散文
汇编器用不了。所以这里发的是和真实压缩同形状的请求(同样的 purpose: "compaction"、
同样的系统提示、真正的 transcript),不是一句 “hello”——后者在真实压缩失败时会通过。
三条性质:
- 不改动任何东西。 不写会话日志、不碰缓存、不动 job 状态,可以在这条对话上随便按。
- 耗时由 Host 测量,不含页面自己的往返,所以跨浏览器可比。
- 长度可选:极小 200 / 小 2k / 中 100k / 大 300k / 全量 900k。小模型在小输入上通过、 大输入上失败,这个差别正是「先测试再托付」的意义。极小档是便宜的连通性冒烟测试, 只证明路由会回话,不证明它能压缩一段真实历史——那个答案在大档位里。
- 最大档不是 1M。 窗口在输入和输出之间共享,1M 窗口的模型也要给输出预算和系统提示 留位置。按满窗口发过去,任何模型都会在预检里被判「测不了」,这一档就永远不可能通过。 900k 留出约 100k 余量,1M 窗口的模型能真正跑起来。
超时随档位放大(1.5 分钟到 15 分钟):一次 900k 的 prefill 会先排队好几分钟,固定超时会把 健康的模型报成坏的。档位超出模型声明的上下文窗口时,在发请求之前就拒绝,说明窗口多大, 并指出哪一档还能用——上传几兆字符后才从 provider 拿到错误,既慢又费钱。
reasoning level 不受支持时会在发请求之前拦下,并列出该模型实际支持的档位——
不会花掉一次请求才发现配错了。
失败会显示阶段(resolve / validate / request)和原始错误信息,成功会显示
输入/输出 token、模型返回的摘要开头。两个结果各占一行,互不覆盖,方便对比。
两个手动按钮
聊天输入框上方的原生上下文弹层里有两个按钮:
[ 手动压缩 ] ← 模型路径(原有行为)
[ 手动非模型压缩 ] ← 本地折页:不调模型、不花 token
第二个按钮在没有配置压缩模型时也能用,那正是本地折页唯一可行的场景。
「手动非模型压缩」默认需要点两次:第一下把按钮变成「再点一次确认折页」(4 秒后自动还原), 第二下才真正折页。关掉设置里的「折页二次确认」就是单击立即执行。
输入栏左侧不再有闪电按钮。它原本是上下文压力表和折页快捷键,但同一个折页动作在两个 地方各有一个入口,而压力读数在上下文弹层里已经能看到。
折页不是「丢弃」
机械折页同样走 compactSurfaceRegion,所以日志里得到与模型路径完全相同的
compaction/start + compaction/summary + compaction/end 标记三元组、相同的
surface op 形状和相同的稳定性检查。区别只在摘要器:foldSummary 从重放的消息里
直接构造短卡,不发网络请求。
这保证了折过的会话对历史 UI、撤销和其他压缩路径仍然可读。
设置页
设置 → 压缩管理,五个 tab:
- 模型 — saviour 开关、压缩方式、救援前失败次数、备用模型 1 与备用模型 2
- 触发线 — 自动压缩的 80%、headroom、retain,以及算出的实际触发点
- 入境 — 工具结果定形阈值、密图预算、密图节省比例
- 折页 — 引擎选择、二次确认、折页卡示例
- 诊断 — 模型压缩 / 非模型压缩两组计数
改动即时保存
没有保存按钮,任何改动都会在 250ms 后自动写入。为什么不是每次击键都发请求:
- 合并连续改动。 数字框拖动或连点几个开关只发一次请求。
- 串行化写入。 Host 每次保存都要解析模型元数据,两个请求并行可能乱序落地, 留下旧配置。
- 不被回包覆盖。 Host 会回显存下的配置;如果用户在往返期间又改了别处, 直接采用回显会把他刚改的字段改回去。只有「发出后没再改过」才采纳回显。
- 失败可见。 被 Host 拒绝(比如模型只选了一半)时,界面上的控件已经显示新值, 所以必须用红色错误条说明「未保存:<原因>」,且不自动消失。成功提示 「已自动保存」会在 2 秒后淡出。
触发线 tab
三个输入共同决定自动压缩什么时候发生,单独看任何一个都没有意义:
消息预算 = 窗口 − 输出预留
触发点 = min( 窗口 × 触发线比例 , 消息预算 − headroom )
保留原文 = 消息预算 × retain
headroom 是绝对值(默认 65536),所以同一个 80% 在不同窗口上含义不同:
| 窗口 | 触发点 | 由谁决定 |
|---|---|---|
| 1,000,000 | 800,000(80%) | 触发线比例 |
| 200,000 | 101,696(50.8%) | headroom |
| 131,072 | 32,768(25%) | headroom |
页面不自己重算这个公式,而是向 Host 要结果(?threshold=1),保证「页面承诺的」
和「引擎执行的」是同一个函数。算出的触发点、两个候选值、由谁决定,都直接显示。
thresholdOverride 默认开启,但三个默认值与 compaction-basic 自带配置完全一致,
所以不修改就等同于没接管。关闭则完全沿用官方配置。
保留比例如果接近触发线,保留段会吞掉整个可压区域,selectCompactableRange
每次都找不到可压内容。这种情况页面会显示已自动下调,而不是静默地永不压缩。
诊断页
诊断页的计数全部是本进程内的累计值,宿主重启后归零,且不分会话。
按插件实际工作方式分两组,两组互斥——「非模型压缩」里没有任何一项花 token, 「模型压缩」里没有任何一项省 token:
模型压缩:压缩次数、失败、共省、最近一次(前 → 后),以及一行上次用的模型
「上次用的模型」显示的是实际产出摘要的那个模型,取自提交结果而非设置。回退发生时 它显示备用模型 2,并用警告色标出「模型 1(x/y)失败,已用备用模型」——把两个模型都写出来, 是因为只看幸存者会掩盖回退曾经发生过。
非模型压缩:手动折页、接管折页、钩住引擎、已定形、其中贴图、定形共省、 定形失败、状态、会话 API
手动折页和接管折页分开计数:合并成一个数会掩盖 replaceDefault 是否真的生效过。
命令
/fast-compact # 按 engine 设置折一次
/fast-compact ledger # 强制机械折页(不调模型)
/fast-compact saviour # 交给压缩模型
/fast-compact status # 当前设置与可用性
折页不可展开,但原文没丢
官方压缩核心 @deepseek-ai/dsh-compaction 只导出前向能力,没有 undo / revert,
所以折叠之后没有「还原」按钮,任何插件都给不了。
但原始内容没丢:会话日志是只追加的,compaction/summary 事件的 shadowedSeqs
记下了每条被替换的事件号,原始事件永远在日志里,只是 surface 不再引用它。
需要原文时直接读日志文件、按 seq 定位即可——不需要插件提供命令。
入境定形
只裁当前回合紧邻上一步、还没发送的大 tool_result。已经发出去的节点逐字节不动——
改写它们会让 provider 缓存前缀失效,代价是整段重新 prefill。
安装
dsh plugin add dsh-compact-manager
或从本地源码链接:
dsh plugin --profile <名称> add link:/path/to/dsh-compact-manager
装好后在 设置 → 压缩管理 配置。插件包裹 live 的
@deepseek-ai/dsh-compaction-basic 引擎实例,所以不要卸载 compaction-basic——
官方的事务标记、surface op 和稳定性校验仍然全部来自它。
开发
node tools/build-client.mjs # 重新生成 lib/client.js
npm test # 单元测试
npm run probe # 客户端/宿主/静态三项检查
lib/client.js 是生成物,CI 会重新构建并检查 diff,所以改前端请改
tools/client-app.js 与 tools/style.js。
目录
lib/
index.js Host 入口:命令、HTTP、远程服务、入境钩子
config.js 合并后的 Config schema
controller.js 策略选择 + 两条压缩路径
compact-core.js 压缩事务(vendored)
pressure.js token meter 装饰器(vendored)
routes.js 压缩路由租约(vendored)
typert.js 远程描述符
ledger/ 折页、入境、密图、字库
client-vendor/ saviour 的圆环/压力表/动画原语(vendored)
tools/ 客户端构建、探针
test/ 单元测试
许可
MIT。包含的第三方代码及其原始版权声明见 NOTICE。
No comments yet. Be the first to write one.