Skip to content

dsh-command-context-trim

Verified

dsh-command-context-trim · v0.4.9 · MIT · Web UI

Model-free /trim for DeepSeek Harness — drop the oldest, least valuable span of context on demand (the `/trim` command) or automatically when a request hits the model's context wall, without any model call.

Install

dsh plugin add dsh-command-context-trim

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

Source

Tags

Readme

dsh-command-context-trim

English →

给 DeepSeek Harness 增加一个不调用任何模型的 /trim: 在真正溢出之前,把对话里最旧、最不重要的一段上下文裁掉,让会话能切到窗口更小的模型上继续跑;同时可以按路由 调优 DSH 自己的 compaction 触发点与工具结果裁剪阈值。

本文是 README.md 的中文同步版:同结构、同命令、同默认值、同公式、同验证事实(长段落做了压缩翻译)。 版本号见上方 synced-with-readme 标记,test/docs-sync.test.js 会检查它与 package.json 一致、且关键内容不漂移。

为什么需要它

把长会话从大窗口的云端模型切到小窗口的本地模型时,下一个请求就会超过新模型的窗口。既有做法是切换前先 /compact —— 而这正是痛点所在:compaction 是摘要,它的摘要请求必须既能塞进(已经变小的)窗口,又要容纳那段 正被摘要的内容;窗口越小,摘要越容易失败,或摘要质量越差(要压缩的信息越多)。/trim 换了一条路:先把最旧的 上下文裁掉,不调用模型、不产生摘要,于是后续 /compact 更便宜、也更容易成功。

它给本地模型带来什么

DSH 的压缩触发点是 min(thresholdRatio × contextWindow, contextWindow − R − headroom),其中 R 是该路由自己的输出预留,而 headroom 固定为 65536,与窗口大小无关。在百万 token 的云路由上这几乎无所谓;在本地卡上它决定一切:

  • 131072 窗口:固定 headroom 让触发点落在 37.5 %,而不是比例想要的 80 % ⇒ 压缩来得又早又频繁;
  • 本插件把该路由的 headroom 设为 0(比例重新说了算)⇒ 触发点回到 104857(80 %);
  • 效果就是更少、更晚的压缩:同样一段工作,压缩次数下降、每次压缩要处理的量更小。

什么时候它不划算

上面那份收益是"更少、更晚"的收益,只在 stock headroom 把一个本可支持的触发点压低了的地方存在。当 stock headroom 直接把触发点关掉时(消息预算 ≤ 65536),把它打开并不会减少已有的压缩,只会增加事件 —— 我们实测 同一条任务从 15 次压缩涨到 24 次。因此 tuneStockDisabledRoutes 默认为 false:这类路由保持原样,继续靠 /trim 与 DSH 自己的溢出路径兜住窗口。

preset 路线是上锁的,而且必须保持可解析

会话一旦开始,它的 preset 就固定了(agent-preset/locked),fork 也继承父会话的 preset 并被同样上锁: buildForkSeed 会把父会话的 turn/start 事件复制进子会话(dsh-session/lib/index.js:884-893),而 fork 本身要求 父会话已有一个完成的 turn,于是子会话的 turnBoundary.lastTurn > 0 ⇒ 同样换不了 preset。因此:

  • 想要新参数:让进程重启即可 ✔ —— 会话的 preset id 是粘住的 ✗(它来自会话的 agentPreset 投影 ✔,而 assertPresetUnchanged 会拒绝另一个 id ✗),但该 id 背后的定义在每次 adopt 时都会被重新解析 ✔: createOrAdopt 与 resumeObserved 都会调用 composeAgent(presetForObservation(observation)) ✔,再把新组合交给 agents.resume ✔,而 retain(id) 返回的是当前定义所激活的那一代 ✔。所以"改 preset 时没有在运行"的会话, 只要 resume 就会吃到调优 ✔ —— 不需要 fork ✗。

只有一种情形需要 fork ✔:改定义时会话仍在运行 ✗。活着的 agent 绑在它当初组合的那一代上 ✔,那份绑定会让旧 realm 保持 存活 ✗,所以改动到不了它 ✗ ⇒ 这时才 fork(或等下一次重启 ✔)。fork 是新 session id ✗、且自身也被锁 ✗(继承父会话的 turn/start ✔)—— 但这对它无害 ✔,因为它已经在跑调优后的值 ✔。

该用哪条路线

两条路线都能设置全部压缩参数 —— 触发点、pruner、以及摘要模型(summarizationProvider / summarizationModel 就是 compaction-basic 上的普通键,可用 modelPolicies 按路由设置)。优先 profile 平面:它每次启动都能重新调优、 且不涉及 preset 的持久性负担。

路线 覆盖
profile 平面(headless / tui:autoTuneCompaction、/trim tune) 自动路径确实会触发 ✔:host 平面拥有 agent ✔
preset 平面(web:/trim preset inplace + check + fork) 手动 ✔:会话 agent 在各自 preset 域里创建,其生命周期事件到不了 host 平面 ✗(已实测 ✔)

安装

# 从 npm
dsh plugin --profile web add dsh-command-context-trim

# 从 checkout
dsh plugin --profile web add file:/path/to/dsh-command-context-trim

用法

/trim                       # 按当前模型窗口裁剪
/trim check                 # 只报告计划,不改动
/trim 32k                   # 指定 32768 token 预算
/trim 40000                 # 指定 40000 token 预算
/trim provider:model        # 按另一条路由的窗口计算
/trim tune [check]          # 用当前活跃路由重新调优本进程的 compaction 行(profile 平面)
/trim preset [check|list|default|inplace|p:m]
/trim rescue <id> [--from <donor>] [--untuned]

/trim preset 的完整形式:

/trim preset              # 依据 profile 里配置的路由生成并落地一个调优 preset
/trim preset inplace      # 覆盖**基础 preset 自己的 id**,而不是新增一个 id(见下)
/trim preset check        # 打印将生成的行 + 路由覆盖率,不写任何东西
/trim preset list         # 会覆盖哪些路由、各自拿到什么触发点
/trim preset default      # 同时把它设为**新会话**的默认 preset
/trim preset p:m          # 本次用 p:m 作为摘要路由
/trim rescue <id>         # 用同 id 重建丢失的 preset(--from <donor>、--untuned)

/trim preset check 是过期信号:它统计当前 preset 覆盖了多少条路由,并对消息预算 ≤ 64K 却未被覆盖的路由 给出明确告警 —— 那是唯一一种"继承顶层调优值反而打开压力触发"的情况。新增模型后跑一次 check,报缺口就重跑 /trim preset inplace:

coverage: 4 route(s) configured, 3 covered by this preset, 1 not covered.
⚠ uncovered SMALL route bonsai-8gb//models/mtp-lean.gguf (40960 − 8192 ≤ 65536): without a policy it inherits the
  tuned top level, which enables the pressure trigger on a route where that measured slower. Re-run /trim preset inplace…

工作原理

一次 trim 是两次同步相邻的追加:先写入一条替换消息(带 plugin:dsh-command-context-trim 来源标记), 再写入 token meter 的测量事件;两者之间没有 await,所以模型不会看到中间态。裁剪按从最旧开始的优先级进行, 最后一条消息降级为"尽量不裁",用户指令始终受保护。

上下文墙上的自动裁剪

autoTrim(默认开启)让同样的免模型裁剪在无人敲命令时发生:一个 prepend 的 agent/request-error 监听器 响应 CONTEXT_WINDOW_EXCEEDED,裁剪,然后请循环重试。它不在普通压缩时触发。

请求失败(上下文墙)
  → 裁剪并重试(最多 maxAutoTrimRetries 次)
  → 仍失败 ⇒ 交给 DSH 自己的压缩路径

受保护的内容

用户指令(最后一条消息)尽量不裁;protectHeadNodes(默认 1)保护开头的节点;保留尾部(retainRatio / retainTokens / minTailTokens)逐字保留最近上下文;allowTailTrim 控制最后手段的层级。

兼容性

session format v4 下的标记来源。 替换消息携带来源 kind plugin:dsh-command-context-trim —— v4 要求的 plugin:<plugin id> 形状 —— 因为 0.1.7 的准入路径会拒绝已退休的 {kind: 'plugin'} 包装,报 format v4 message requires a producer-owned source kind。

设置卡片

Plugins 页面上的卡片显示的是生效值,而不是"是否覆盖":部署没设过的字段会显示插件真正会用到的默认值——因为一片空白 的控件正是"我的新模型为什么还没调优"变成无解问题的原因。卡片分两个分区:压缩(触发阈值、摘要路由、两个自动调优 开关)与裁剪(工具结果裁剪阈值),而裁剪阈值是"选值下拉 + 数值框":只有选 Custom 时数值框才出现。

选值 落到设置面的值
Disabled 0(不动 pruner)
Auto auto(按路由窗口推导)
Custom 你填进数值框的字符数

运行时自动调优压缩阈值 上以粗体写着唯一要紧的前提:仅限 headless profile —— web profile 需使用 /trim preset 命令来调优 (web 里会话 agent 在各自 preset 域内创建,其生命周期事件到不了 host 平面)。开关用的是壳子自带的 Switch 原语;选值框是 原生 <select>(原语没有 Select)。

配置

在 profile 补丁的 context-trim 行上覆盖(随包 cordis.patch.yml 列出了完整默认值):

键 默认值 含义
targetRatio 0.9 budget = floor((contextWindow - reserveOutputTokens) * targetRatio)
reserveOutputTokens 8192 留给模型自己回复的输出空间。它与服务端自己的 maxTokens 之和要落在真实窗口内:一个 32k 的服务若 maxTokens: 16384,即使提示词都装得下,提示词 + 输出也会耗尽窗口
retainRatio / retainTokens 0.16 / — 逐字保留的最近尾部(两种形式互斥)
minTailTokens 2048 该尾部的绝对下限
protectHeadNodes 1 永不裁剪的开头节点数
allowTailTrim true 启用层级 2–3(伸进保留尾部;最后手段可含末条消息)。false 表示只做层级 1,保留尾部成为硬边界
markerSlackTokens 64 加在计价标记上的余量,保证裁剪后的请求仍在预算内
autoTrim true 在 CONTEXT_WINDOW_EXCEEDED 时自动裁剪;绝不在普通压缩时触发
maxAutoTrimRetries 3 每次溢出允许的自动裁剪次数,之后交给压缩
autoTrimShrink 0.5 重复溢出后,按被拒请求的这个比例重新取预算目标(声明窗口有误时的几何下降)
preferInPlacePrune true 规划任何区间之前,先就地瘦身过大的工具结果;能用到官方 pruner 时就用它
compactionTargetRatio 0.8 /trim preset 写进 preset 的触发比例(只塑造 preset,从不影响本插件自己的裁剪)
compactionRoute 未设置 生成 preset 的摘要调用所用的可选 {provider, model}
tuneStockDisabledRoutes false 为 false(默认)时,stock 配置里没有压力触发的路由保持原样 —— 在那里打开触发只会增加压缩事件(见什么时候它不划算),所以免模型的 /trim 与 DSH 的溢出路径继续兜住窗口。设为 true 才会一并调优这类路由
prunerThresholdChars auto(DSH_TRIM_PRUNER) 工具结果裁剪阈值。auto 按路由派生为 max(8192, min(32768, 2 × (contextWindow − maxTokens)))(各路由取最小),小窗口下一次整文件读取因此不会被裁掉(DSH 的 stock 值是 8192)。整数覆盖它;0 表示退出、保持不动。与 compaction 行同一平面,因此同样的可达性规则适用 —— web profile 把 pruner 放在每个会话的 preset 里,插件会报告这一点而不是写入。小窗口上这是最关键的杠杆(见什么时候它不划算);压力触发已启用时它会放大提示词,那里要三思
autoTuneCompaction false 重新调优本进程的 compaction 行(仅在 compaction 位于 profile 平面时)。web 或生产 profile 应该设置这一项(通过设置卡片或补丁层);覆盖它的 DSH_TRIM_AUTO_TUNE 环境变量只用于自动化/CI
pruneThresholdChars / pruneHeadChars / pruneTailChars 8192 / 4096 / 1024 就地瘦身的预算,镜像 DSH 自己的 pruner 默认值

环境变量(供自动化/CI 覆盖对应配置项):DSH_TRIM_AUTO_TUNE、DSH_TRIM_PRUNER、DSH_TRIM_TUNE_STOCK_DISABLED。

权限与失败边界

插件触碰了什么,写在这里以免审阅者靠猜:

  • 只写两条会话事件(替换消息 + 测量),不调用模型;
  • 读取 profile 补丁、会话记录与 token meter;
  • /trim preset 与 /trim rescue 会写 profile 补丁(写前备份为 <patch>.bak-trim-preset);
  • 自动路径只在 profile 平面可达时写入;不可达时只报告,不静默失败。

边界

  • 它是丢弃内容,不是摘要。 被裁掉的细节从模型视野里消失(仍在日志里)。想要摘要就用 /compact;两者互补 (先 /trim 会让之后的 /compact 更便宜、更可能成功)。
  • 最后一条消息尽量不裁(见 allowTailTrim):保护用户指令优先于塞进预算。
  • /trim 不改变 preset,preset 也不能在会话中途更换 —— 要新参数就 fork 或新开会话。

调优压缩触发点(/trim preset)

DSH 依据 thresholdTokens = min(contextWindow × thresholdRatio, messageBudget − headroomTokens) 决定何时压缩, headroomTokens 默认 65536。在约 370k 以下的任何窗口上,决定触发点的是这个默认值而不是比例:131072 的路由在 37.5 % 就压缩,而不是 80 %。单靠比例改不动它,而 compaction 的策略是在 preset isolate realm 里组合时读取的, 插件无法在运行时改动它。因此 /trim preset 写的是一个 preset:

# >>> dsh-command-context-trim: preset-standard (generated; delete this block to drop the preset) >>>
- id: preset-standard
  name: '@deepseek-ai/dsh-agent-preset'
  config:
    id: standard
    plugins:
      - id: compaction-basic
        name: '@deepseek-ai/dsh-compaction-basic'
        config:
          thresholdRatio: 0.8
          headroomTokens: 0
          modelPolicies:
            - provider: b70-sycl
              model: /models/q.gguf
              thresholdRatio: 0.8
              headroomTokens: 0
# <<< dsh-command-context-trim: preset-standard <<<

inplace 写的是裸覆盖行(- id: preset-<base> + 完整 config:):补丁层按 id 整块替换该行的 config, 所以它顶掉随包 preset 的插件清单,新会话继续用同一个 preset id、不需要任何人去挑。代价:随包那份每次升级都会 被覆盖,而这一行会遮蔽它,直到你删掉这段标记块。

运行时重新调优(/trim tune)

在 compaction 位于 profile 平面的地方 —— 任何基于 dsh-base 的组合,即 headless 与 tui —— 它的阈值就是某一行 的普通 config,cordis 通过重启 fiber 来应用 config 变更(Fiber.update() 解析新配置并调用 restart(),即 dispose 加一次全新 apply)。所以 /trim tune 是有效的:它改的是那一行。web profile 里 compaction 在会话的 preset 域内, host 平面碰不到它 —— 插件会报告这一点(/trim preset 才是那里的答案)。

自动化调优阈值(headless 运行)

headless profile 自己组合 compaction —— 它的树在 profile 平面上带 compaction-basic、command-compact 与 tool-result-pruner,而且它从不解析会话 preset(只有 session API、web 客户端与 agent-preset 行会)。 实测(干净 DSH_HOME):在路由上声明 contextWindow 并设 DSH_TRIM_AUTO_TUNE=1 后,该行的 thresholdRatio/headroomTokens 与 pruner 的 thresholdChars 都会被写成派生值,且带 context-trim/tuned 记录 可审计。web 侧对应的是手动流程(见该用哪条路线)。

读会话日志

compaction/prune 有两个生产者,只有一个是本插件:本插件在就地瘦身时写入自己的记录,DSH 官方的 tool-result-pruner 也写同名事件。区分办法是看事件的来源与伴随字段;本插件还会写 context-trim/tuned 记录, 里面带有它算出的 thresholdChars(这也是我们验证 Bonsai 2 那次"17 → 2"的依据)。

开发

它放在 fixtures/ 而不是 test/ 下是有意的:node --test 会执行 test/ 下的每个 JavaScript 文件,把服务器放在那里 会让测试挂住。

端到端溢出检查(不需要模型)

fixtures/mock-overflow-server.mjs 是一个有状态的 OpenAI 兼容端点:它强制一个比告诉 harness 的 contextWindow 更低的真实上限,并且对前 TOOL_STEPS 个请求回以工具调用,于是一个 turn 会不断循环、越过真实上限 —— 这就是 上下文墙,且不需要切换模型。

验证状态

见英文 README.md 的 Verification status 表(149 个测试、隔离实例验证、0.3.9–0.4.4 的各项证据,以及 Bonsai 2 headless 的前后对比:thresholdChars: 32768、compaction/prune 17 → 2)。中文版不重复该表以免两处漂移; test/docs-sync.test.js 会检查本节仍然指向它。

许可证

MIT


计算细节

本插件算出的每个数字,以及它从哪个默认值出发。token↔字符换算用 DSH 自己的 CHARS_PER_TOKEN = 4 (@deepseek-ai/dsh-token-meter)。

1. 裁剪预算(/trim 把请求面塞进多少)

usable  = contextWindow − reserveOutputTokens          # reserveOutputTokens: 8192
budget  = max(1, floor(usable × targetRatio))          # targetRatio: 0.9
retain  = max(minTailTokens,                           # minTailTokens: 2048
              floor(contextWindow × retainRatio))      # retainRatio: 0.16

2. 规划器在比较什么

surfaceTokens  = Σ node.heuristicTokens                       # token meter 给每个请求面节点的定价
envelopeTokens = measurement.totalTokens − measurement.surfaceTokens   # 工具 schema 等固定请求数据
totalTokens    = envelopeTokens + surfaceTokens

3. 压缩触发点(自动调优与 /trim preset 写入的值)

镜像 @deepseek-ai/dsh-compaction-basic 的 resolveCompactSpec(按 dsh 0.1.7-rc.2 读取):

reservedCompletion = 该路由请求的 maxTokens      # 输出预留,从 request/header 读
messageBudget      = contextWindow − reservedCompletion
headroom           = headroomTokens             # stock 65536;我们写 0
pressureBudget     = messageBudget − headroom
thresholdTokens    = floor(min(contextWindow × thresholdRatio, pressureBudget))

4. 工具结果裁剪阈值(0.3.5 起由自动调优派生)

prunerThresholdChars = max(8192, min(32768, 2 × (contextWindow − maxTokens)))

即一条工具结果最多可占该路由消息预算的一半,上限 32 KB、下限为 DSH 的 stock 8192。各路由取最小,因此 最严格的那条说了算。

5. 样例

路由 stock 触发点 调优后 压力触发
Bonsai 40960 / 8192 无(32384 ≤ 65536) 保持原样 无
Bonsai 40960 / 16384 无(24576 ≤ 65536) 保持原样 无
131072 / 16384 49152(37.5 %) 104857(80 %) 有
1000000 / 256000 678464(67.8 %) 744000(74.4 %) 有

6. 自动裁剪

autoTrim: true 通过缩小预算来重试失败的请求: nextCeiling = max(1, floor(failingTotal × (1 − autoTrimShrink))),autoTrimShrink: 0.5,最多 maxAutoTrimRetries: 3 次,之后放弃并报告它测到的东西。