Skip to content

dsh-compaction-threshold

Verified

dsh-compaction-threshold · v1.0.3 · MIT · Web UI

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

Install

dsh plugin add dsh-compaction-threshold

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

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

Creators

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)且带同源 标记的请求。