Chuyển đến nội dung chính

dsh-compaction-threshold

Đã xác minh

dsh-compaction-threshold · v1.0.3 · MIT · Giao diện web

在上下文监控卡片内直接调节自动压缩的触发阈值。host 半在运行时替换 @deepseek-ai/dsh-compaction-basic 已挂载引擎的 thresholdRatio 并持久化,client 半把滑块画进卡片面板。

Cài đặt

dsh plugin add dsh-compaction-threshold

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.

Thẻ

Tác giả

Readme

dsh-compaction-threshold

在 DSH 的上下文监控卡片里直接调节自动压缩的触发阈值,并查看本会话的压缩记录。

点开输入框下方的上下文占用卡片,面板底部会多出:

  • 一条「压缩触发阈值」滑块,拖动即生效,无需重启;
  • 一个「压缩记录」入口,展开后按时间倒序列出本会话每次压缩前后的上下文占用。

它改的是什么

自动压缩由 @deepseek-ai/dsh-compaction-basic 驱动,触发条件是

thresholdTokens = floor(min(contextWindow * thresholdRatio,
                            contextWindow - reservedCompletionTokens - headroomTokens))
measurement.totalTokens >= thresholdTokens  →  压缩

滑块写的就是 thresholdRatio。默认 0.8,即上下文用到窗口的 80% 时压缩; 调低更早压缩、更省窗口,调高则更晚压缩、单轮上下文更完整。

范围 20% – 95%,下限还会自动抬到各引擎 retainRatio 之上(官方 resolveCompactSpec 要求保留比例严格小于触发比例,两者相等会抛错)。

口径:以卡片为准

只看 thresholdRatio 会有一个坑:官方存在两套不同的上下文口径。

算法 特点
卡片 contextPressure 投影的 projectedTokens ?? pressureTokens 主项是 provider 回报的真实 prompt usage(不含输出)
引擎 TokenMeter.measure().totalTokens 真实 usage 与 4 字符/令牌的启发式估值取高者

真实中文/代码内容的密度约 5.4–6.9 字符/令牌,而启发式硬编码 4,于是它几乎 总是高估(实测 33%–73%),并且总是赢得那个「取高者」判断。结果就是引擎读数 长期高于卡片:卡片才 50% 出头,引擎已经越过 80% 阈值 —— 表现为「明明设了 80%,却在 60% 左右就压缩了」。

本插件把 TokenMeter.measure() 的 totalTokens 对齐到卡片那个数,于是 滑块比例与卡片显示的百分比成为同一件事:卡片到 80% 才压。

对齐只做三件最小的事:

  • 只影子 meter 实例自身的 measure,不碰原型;
  • 只替换 totalTokens,nodes 原样透传 —— 压缩选区 (selectCompactableRange)与 surface 稳定性校验只读 nodes;
  • 传 requestHeader 的推测调用一律不介入(卡片只为已落盘的事实作答)。

contextPressure 是纯事件折叠、不回调 measure,所以不存在递归。投影或 meter 不可用时原样退回引擎读数,绝不影响压缩本身能否发生。

headroomTokens 被强制为一个负值(-1e6)。官方默认是 65536,在 200K 窗口上 会把压力上限压到约 64%;但置 0 并不够——resolveCompactSpec 的第二支是

contextWindow - reservedCompletionTokens - headroomTokens

而 reservedCompletionTokens 取路由模型自己声明的 maxTokens。本机 wk 路由的 buddy:deepseek-v4.1-flash 声明了 maxTokens: 128000,于是这一支变成

200000 - 128000 - 0 = 72000        // 仅 36%

min() 在上面那一支里永远选它,滑块高于 36% 的部分全部失效。日志里的真实触发点 是 72196 和 88293 令牌(36% / 44%),而不是 160000。

负的 headroom 直接把这份预留抵消掉:contextWindow - reserved - (-1e6) > contextWindow, 第二支再也无法成为较小者,比例重新成为唯一决定项。它不是一个预算,只是一次抵消, 所以量级只需超过任何真实 contextWindow 即可;摘要请求自己的输出上限由 maxTokens 单独重述,不受影响。

输出预算兜底

DSH 在请求发出前会调 clampMaxTokensToContext 收缩输出上限:

available = contextWindow - estimateContextTokens(...).tokens - CONTEXT_SAFETY_TOKENS(4096)
max_completion_tokens = min(maxTokens, max(1, available))

estimateContextTokens 在拿不到 provider usage 时退回 4 字符/令牌的启发式, 而真实密度约 5.4–6.9,于是长期高估约 1.3 倍。上下文一涨,available 就被算没, 输出上限被压到几十个令牌,表现为模型刚开口就停、并弹出「已达到输出 token 上限」。

这不是一个点而是一段区间:只要 available < maxTokens 就开始压,在 200K 窗口 + 128000 上限下,真实上下文约 51K 起上限就一路下滑。实测(session 524bf8d2):

真实上下文 estimate 输出上限产物 实际输出
148408 195857 47 47
148408 195886 18 18

修在 globalThis.fetch 层——它是唯一位于 clamp 之后的位置,改 model、改配置 或修估算都要跟那个 Math.min 相斗,而这里改的是最终离开进程的字节。判定线是 clamp 的产物本身(<= 16384,等价于估算口径超过窗口 89%),命中的字段抬回 outputFloorTokens(跟随压缩器实际的 maxTokens,默认 65536)。正常配置值 (65536 / 128000 / 256000)远高于判定线,不受影响。

压缩记录

compaction-basic 每压缩一次都会往会话里写三个事件:

compaction/start  →  compaction/summary  →  compaction/end

host 半监听 host 平面的 session/event,在 start 时量一次「压缩前」,压缩 结束后量一次「压缩后」,连同 summary 里的 shadowedSeqs(折叠了几条消息) 与 shadowedTokenCount 一起落盘。因为读数走的是上面已对齐卡片的 measure(), 日志里的百分比与卡片当时显示的百分比一致。卡片里每条记录显示:

10/04 21:05 · 第 7 轮
~160K (80%) → ~42K (21%)
折叠 12 条消息 · 省下 ~118K

「压缩后」不是取 compaction/end 那一刻,而是等下一次 provider 回报 usage 之后 再定稿:surface 替换发生在 usage 刷新之前,此时投影仍持有压缩前的 surface 印记, projectedTokens 会被夹到接近 0(实测 end 时 1%,一个 usage 采样之后才是真实 的 17%)。end 时的读数只作为「会话到此为止」的兜底。

压缩失败(compaction/end 带 error)也会记录,并把失败原因标红显示。日志 按会话过滤:面板是 portal 到 document.body 的,与会话根节点分离,所以靠 「当前展开的那个 trigger」回溯它所在的 [data-conversation-session] 拿会话 id。

tokenMeter 取不到时(例如引擎尚未挂载),记录仍会写入,只是前后读数显示为 「未知」——量不出来不该影响压缩本身。

安装前的历史压缩

插件的日志只从装载那一刻开始记。但三个压缩事件是持久化在会话日志里的,所以 面板首次打开某个会话时会回填该会话此前的每一次压缩,行首标一个「历史」小标签 以示区别。

回填不是近似:measure() 与 contextPressure 投影都是对事件列表的纯折叠, 把会话事件截断到某个历史 seq 重放,得到的正是卡片当时显示的那个数。因此历史行 的百分比与当时的卡片读数一致。

回填按 compactionId 与已有记录去重,可重复执行;已由实时路径记录过的压缩保持 其「实时」身份,不会被历史行覆盖。

为什么不是改写官方文件

桌面版 runtime 封在只读的 app.asar 里,条目带 SHA256 integrity, 改写官方库文件无效也不安全。本插件是纯 cordis 插件:

  • host 半(lib/index.js)在运行时替换已挂载压缩引擎实例上的 config 引用。引擎每次判定都重新读 this.config,所以下一次 agent/pre-step 就按新阈值决策。
  • 官方把 compaction 挂在 isolate 组里,组外 ctx.get("compaction") 解析 不到实例;但 isolate 只拦截上下文代理取值,fiber 自身的 store 是普通对象, 因此 host 半遍历 ctx.registry 的 fiber store 拿到全部存活引擎(已用真实 cordis 验证)。
  • client 半(lib/client.js)把控件追加进卡片面板。面板内容写死在 dsh-client-ui-conversation 的 bundle 里、内部没有 slot,所以用 DOM 增强, 并以 MutationObserver 兜底重挂。

安装

dsh plugin --profile desktop add dsh-compaction-threshold

或手动:把包放进 $DSH_HOME/profiles/<profile>/node_modules/,并在该 profile 的 cordis.patch.yml 里加一行

- insert:
    - id: compaction-threshold
      name: 'dsh-compaction-threshold'

- insert: 行不按 id 去重,不要重复挂载,否则插件会注册两次。

状态文件

  • 阈值:$DSH_HOME/compaction-threshold.json,跨重启保留。
  • 压缩记录:$DSH_HOME/compaction-log.json,最多保留最近 200 条。

HTTP 接口

/compaction-threshold/api/state

  • GET → { ok, value: { thresholdRatio, minRatio, maxRatio, defaultRatio, engines } }
  • POST { thresholdRatio } → 持久化并立即应用到所有存活引擎

/compaction-threshold/api/log

  • GET → { ok, value: { entries, total, limit } },最新在前
  • GET ?session=<id> → 只返回该会话的记录

仅接受 loopback(或 webRuntime.trustedHosts 中配置的 authority)且带同源 标记的请求。