zhuto666/dsh-compact-agents ↗★ 1

dsh-compact-agents

DeepSeek Harness 会话上下文强制压缩插件:模型可调用的 compact_agents 工具,忽略自动压力阈值,覆盖进程内所有活会话(主会话/子代理/AgentTeams 成员),忙则排队、本轮结束补压,逐目标回报遮蔽节点数与估算 token 数。Session-context force-compaction plugin for DeepSeek Harness: a model-callable compact_agents tool that ignores the automatic pressure threshold and covers every live session in the process (main session, sub-agents, AgentTeams members alike), queues a busy target and compacts it when its turn ends, and reports shadowed node counts and estimated tokens per target. 适合长对话或多代理任务,通过强制压缩上下文避免超出模型Token限制。

패키지
dsh-compact-agents
호환성
미검증
버전
0.7.3
라이선스
Apache-2.0
최근 업데이트
2026. 9. 15.

설치

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:zhuto666/dsh-compact-agents

dsh-compact-agents

DeepSeek Harness 会话上下文强制压缩插件(模型可调用的 compact_agents)

强制压缩忽略自动阈值 · 覆盖进程内所有活会话(主会话 / 普通子代理 / AgentTeams 成员一视同仁) · 没有子代理时主会话也能压自己 · 忙的目标自动排队、本轮结束立即补压 · 逐目标回报被遮蔽节点数与估算 token 数 · 只有顶层 agent 能扫描、子代理越权被拒 · 工具调用串行不并发 · 压缩过程在对话区可见 · 被输出上限截断时自动续写 · 压缩阈值等参数可在「设置」里直接改 · 零网络、零依赖、浏览器 half 手写无构建步骤

version

v0.4.0:所有压缩相关参数搬进「设置」界面。压缩触发阈值、保留比例、受控阶段输出预算、提示开关、自动续写次数 —— 五项都能在 设置 → 插件 里改,不用再编辑 preset 的 YAML;顺带补上「被输出上限截断时自动续写」,让对话不再停在"已达到输出 token 上限"。详见设计说明。

license dsh node

English | 中文


功能总览

能力入口 / 参数说明
强制压缩(忽略自动阈值)compact_agents(scope)走 ctx.compaction.compactNow,契约原文是*"Explicitly compact useful history even below automatic pressure thresholds"*;自动压缩要等阈值,本工具不等
覆盖所有活会话scope: "others"(默认) / "all"覆盖范围是 ctx.agents.list()——主会话、普通子代理、AgentTeams 成员一视同仁,不是只挑团队成员
主会话压自己scope: "self"没有子代理时也有效:调用者永远在 turn 中,必然走排队路径,本轮结束自动补压
指定目标scope: "ids" + ids: [...]只压列出的会话;找不到的 id 单独回报,不静默吞掉
忙则排队whenBusy: "queue"(默认)目标正在跑 turn 时不放弃:监听其 agent/status → idle,这一轮一结束就补压;"skip" 则直接报 busy 不动它
逐目标回报返回值 results[]每个目标给出 compacted / queued / noop / busy / error,以及被遮蔽的节点数与估算 token 数
越权保护—只有顶层 agent 能扫描他人;子代理只能 scope: "self",成员无法互压或压队长
串行调度—注册为 fail-closed 的 exclusive,一次扫描不会和另一个可能压同一会话的调用并行;超时 30 分钟(排队部分不计入,它在 turn 之后跑)
自动压缩阈值(配套)compaction-basic 配置本插件不改自动策略;安装脚本顺带核对 thresholdRatio(DSH 默认 0.8×1M=800K 等于永不触发;默认 0.35,即 350K 触发)
压缩过程在对话区可见默认开启;notice: false 关闭订阅 session/event,compaction/start 一落地就往会话尾追加一条插件来源的 user/message,客户端渲染成「上下文注入 · dsh-compact-agents」折叠行:压缩中显示 正在压缩上下文…(当前 213,400 tokens)· 触发线 ×0.35,结束时显示 上下文压缩完成:约 213,400 → 49,800 tokens,已遮蔽 37 个历史节点。compaction/start 是在摘要模型调用之前写的,这段提示正好盖住原本什么都看不见的等待。提示里会报出本会话实际生效的触发线;万一热同步没成功,折叠行与正文会直接点明两个值与"新开一条对话才生效"
被输出上限截断时自动续写默认开启;maxAutoContinues(默认 2,0/false 关闭)一轮以 turn/end{reason: 'max-tokens'} 结束时,替用户发一句"继续"(agent.followup,与人在界面上发言同一条路),让对话自己走下去。连续次数有上限,任一轮正常结束即清零,避免无止境烧 token
设置界面里能改默认开启;settings: false 关闭注册 settings 命名空间 compact-agents,浏览器 half 在「设置 → 插件」里提供卡片:压缩触发阈值、保留比例、受控阶段输出预算、压缩提示开关、自动续写次数,五项都能在界面上改,不用再去编辑 preset 的 YAML

为什么需要它

DSH 的手动压缩入口只有一个人机命令 /compact(@deepseek-ai/dsh-command-compact 用 ctx.commands.register 注册,只服务交互式 UI 适配器)。于是:

  • headless 的子代理 / AgentTeams 成员没有命令面,执行不了 /compact;
  • 队长也没有任何工具能替成员压缩——packages/compaction/ 下没有任何 registerTool;
  • 而自动压缩(compaction-basic)是按阈值触发的:阈值没到就不压。

结果就是"会话越跑越贵"过去只能靠换人(退役成员、新建成员)解决。本插件补上这个缺失的模型侧入口。

真实教训:一个成员会话曾以每次调用重发 约 40 万 tokens 的上下文跑到 348 次调用,累计 1.4 亿 cacheRead、¥10.87;而它只是"没人能替它压缩"。

安装

需求:Node.js ≥ 20 + DeepSeek Harness(带 agent-presets 的版本)。

一键安装(推荐)

git clone https://github.com/zhuto666/dsh-compact-agents.git
cd dsh-compact-agents
node scripts/install.mjs --dry-run     # 先预览要改什么(不改盘)
node scripts/install.mjs               # 确认后执行(profile 默认 web,可用 --profile 指定)

脚本做这几件事,且幂等:

  1. 建三个目录联接(junction),让插件能解析到 @deepseek-ai/dsh-tools、@deepseek-ai/cordis 与 @deepseek-ai/schemastery——本项目刻意零依赖(不装 node_modules),Node 会把 junction 解析到真实路径,因此拿到的是和宿主同一个模块实例,没有双实例问题;
  2. 往 preset 的 compaction 隔离组里追加一行挂载(compact_agents 工具、压缩提示、自动续写都靠它):
    - id: compact-agents
      name: '/absolute/path/to/dsh-compact-agents/index.js'   # 安装脚本会自动填成你的真实绝对路径
  1. 把本插件登记进宿主组成(浏览器 half、设置页那张卡片靠它):在 /profiles/ /node_modules/ 下建一个指向本仓库的 dsh-compact-agents 联接,并把 dsh-compact-agents 加进该 profile package.json 的 dsh.profile.bundles —— 宿主 Loader 于是多出一行 compact-agents-client-host(由本包的 dsh.bundle.patch → cordis.patch.yml 注入,入口 client-host.js)。profile 用 --profile 指定,默认 web;改这个 package.json 前会留一份 .bak-compact-agents。

装完请重启一次 dsh:宿主组成变了(profile 多了一个 bundle),重启后「设置 → 插件」里才会出现那张卡片。之后只改 preset 里的参数就不必重启(见「生效」)。

自动发现 $DSH_HOME/.agent-presets/*/agent.cordis.yml 与 $DSH_HOME/profiles/*/node_modules/@linxin666/*/presets/*/agent.cordis.yml;也可以用 --preset 指定要处理的 preset。改文件前会留 .bak 备份,没有 compaction 组的 preset 直接跳过。

DSH 检出位置同样是自动探测的,脚本和文档里不写死任何盘符,依次尝试:

  1. --dsh / $DSH_CHECKOUT / $DSH_HARNESS;
  2. 本项目 node_modules 里已有的联接目标(装过一次就连带记下了检出在哪儿);
  3. PATH 上 dsh 启动器的真实入口(包管理器生成的 shim 里写着 dsh 在哪儿)—— 全新克隆、什么线索都没有时,靠的就是这一条;
  4. 各 profile 的 node_modules;
  5. 家目录下的常见克隆位置。

全都落空才报错,并提示用 --dsh 指定。

那行路径不是写死的,是安装时算出来的 —— install.mjs 用自己所在目录推导 PLUGIN_ENTRY,所以:

  • 你在哪儿克隆/放这个项目,那行就指向哪儿;
  • 项目被移动或改名后,重跑一次 install.mjs 就会自动改正:发现已有行指向一个不存在的路径时直接 repaired;若旧路径仍然有效(例如另存了一份副本),则只 WARN 不动手,加 --force 才重新指向。
node scripts/install.mjs --dry-run    # 预览(不改盘)
node scripts/install.mjs              # 执行;自动修复失效路径
node scripts/install.mjs --force      # 旧路径还有效时也强制重新指向

为什么不能像普通包那样只写包名? 这不是偷懒,是 DSH 的既定语义:preset 行里的裸包名是从 harness 安装位置解析的(agent-presets/src/mount.ts 的 PresetTree.import 注释原文 "a package name resolves from the harness base"),不是从用户目录;装在工作区/用户目录的包根本解析不到。所以第三方插件在这里只有两条路——写绝对路径,或把插件文件放进 preset 目录跟着走。本项目选前者:单一真源,不给每个 preset 留副本。

为什么要挂两处(宿主组成 + preset)

两个挂载点缺一不可,各管一半:

挂载点载体负责
preset 行/.agent-presets/*/agent.cordis.yml 里 compaction 组的 compact-agents 行(绝对路径指向本仓库 index.js)compact_agents 工具、压缩进度提示、被输出上限截断后的自动续写。必须待在 compaction realm 内:cordis 的隔离按服务名生效,realm 外解析不到 ctx.compaction
宿主组成里的 bundle 行profile 的 dsh.profile.bundles 登记本包 → 本包 dsh.bundle.patch 指向 cordis.patch.yml → 插入 id: compact-agents-client-host / name: 'dsh-compact-agents/client-host'(入口 client-host.js)让 DSH 的客户端模块表扫到本包的 dsh.client 声明(从而把 lib/client.js 下发给浏览器),并在宿主根上注册 settings 命名空间

只挂 preset 是不够的:DSH 的 ClientModuleRegistry(packages/client/modules/src/index.ts) 只遍历宿主 Loader 的 entries 来决定给浏览器下发哪些客户端 bundle —— 它监听 internal/plugin 的那段里有一行 const entryName = fiber.entry?.options.name; if (entryName === undefined) return,注释明说"fiber.entry 为空的是子插件或手动挂载",直接丢弃;构造时也只做 for (const entry of ctx.loader.entries())。而 preset 里的行是 agent-presets 用 internal.import 手动挂载的、不是 loader 行。所以只挂在 preset 里,浏览器 half 永远不会被下发,症状是设置页里既没有命名空间也没有卡片、且毫无报错。根因与源码位置见设计说明 §8.4。

client-host.js 这个根入口刻意 inject = []:宿主根上没有 compaction 服务(它由 preset realm 内的 compaction-basic 提供),声明依赖只会让这一行永远 pending。它只做两件事:让客户端模块表扫到本包、在宿主根上注册 settings 命名空间;两处入口都调用 registerSettings,靠模块级缓存保证进程级只注册一次(真实的 SettingsProvider.register 对重复命名空间会抛错)。

这不是自创形态:已装的站外插件 @a9i5k4/dsh-auto-memory 同样声明了 dsh.bundle.patch、dsh.client 与 exports['./client'],patch 内容就是 - insert: [ { id, name } ]。本插件采用与它相同的形态。

生效

改动落在哪一层,决定它怎么生效:

改了什么生效方式
宿主组成(首次安装新增的 bundle 行、client-host.js、cordis.patch.yml、package.json 的 dsh.* 声明)必须重启 dsh —— 重启后设置页里才会出现那张卡片
preset 的压缩阈值 / 保留比例保存即生效:本插件把新值热同步进正在运行的会话(下一次步边界就用新值),同时写进 preset 文件供新会话读取
preset 的受控阶段输出预算(tool-bootstrap)与挂载行新开一条对话即可,不必重启
插件本体 .js必须重启 dsh(ESM 模块缓存,理由见「开发者本地调试」)

preset 改动靠 standing mount 的文件戳热重载(戳 = stat 的 mtimeMs + size):戳变了,下一条新会话重新挂载一代,就带上工具;已经 composed 的会话因 agent-preset/locked 拿不到,属预期。

但阈值与保留比例不必等那一步:本插件订阅设置面的保存事件,读回 preset 里的新值后直接写进正在运行的 compaction-basic 实例 —— 该实例的 config 是普通自有属性(对象本身被冻结,但引用可换),而压力判定每次调用都重新读它,所以下一次步边界就按新阈值判,不用新开对话、更不用重启。bootstrapMaxTokens 属于另一个插件(tool-bootstrap.mjs 在 apply() 里把值捕获进闭包),改不动,仍然只对新会话生效。

压缩提示里会报出本会话实际生效的触发线;只有热同步真的进不去时(配置形状变了/写不进去),才会多一行说明两个值与出路:

⚠️ preset 文件里现在是 ×0.5,本会话这个实例仍按 ×0.2(热同步没成功):新开一条对话才会用上新值。

行配置 livePresetParams: false 可关掉热同步(那就回到"新会话生效"的老行为)。

它还会自我反证(v0.7.1):光"写进去了"不等于"引擎吃了"。热同步之后,插件拿策略自己决定的那次压缩反过来验算 —— 达线判定是 token 数 >= 窗口 × 阈值,所以把阈值从 0.2 抬到 0.5 之后,若 1M 窗口下仍在 250K 就压,说明引擎还按 0.2 判。这时它不再自称生效,而是按 0.2 报触发线,并给出"新开一条对话"这条正路。手动压缩(/compact,或 compact_agents 工具触发的那次)在任意 token 数上都可能发生,一律不当作证据,免得冤枉引擎。

想让现有的成员也被压,就别重启 DSH —— 重启会丢掉所有成员/子代理会话(ctx.agents.list() 只覆盖活着的会话),那就没东西可压了。新开一条对话不影响它们。

于是首次安装有个次序问题:"看到设置卡片"和"别丢成员会话"不能同时满足 —— 先压完再重启,或者重启后重新开成员。compact_agents 工具本身不依赖这次重启:preset 行装好,新开一条对话就能用。

校验安装

node scripts/validate-presets.mjs

逐份报告挂载行位置、引用的文件是否存在、以及 compaction-basic 的 thresholdRatio / retainRatio。期望输出:

.agent-presets/liangshen/agent.cordis.yml
  rows           = compaction-basic,command-compact,compact-agents,tool-result-pruner
  thresholdRatio = 0.35  retainRatio = 0.05
  compact-agents -> /absolute/path/to/dsh-compact-agents/index.js (存在)
ALL OK (4 preset mounted)

设置页将显示哪些初值,可以用只读脚本核对(一个文件都不写):

node scripts/inspect-presets.mjs

更新 / 卸载

git -C dsh-compact-agents pull          # 更新:拉取后重新执行 install.mjs(幂等)
node scripts/uninstall.mjs --dry-run       # 卸载:先预览
node scripts/uninstall.mjs                 # 移除挂载行 + 摘掉 profile bundle 登记 + 删除自己建的 junction

卸载只删自己加的东西:preset 的注释、!!js 表达式、其它行一律不动(实测安装→卸载后文件逐字节回到原状),profile package.json 里的 bundle 登记同样按行摘除、保留原排版。插件目录与 .bak / .bak-compact-agents 备份不删,自行处理。卸载同样改了宿主组成,所以重启 dsh 后设置卡片才会消失。

开发者本地调试

node scripts/integration-test.mjs    # 真机加载测试(真 cordis Context + 真 ToolRuntime)
node scripts/deferred-test.mjs       # 忙→排队→idle 补压 的行为测试(假 ctx)
node scripts/selftest.mjs            # 模块导入 + defineTool 规格自检

改完 index.js 后必须重启 dsh 才能真正生效 —— 这一点很容易踩坑:

  • Node 的 ESM 模块缓存按 URL 命中,preset 重新挂载不会清它(DSH 自己的 HMR 插件是靠显式清 internal.loadCache 才能热重载的,而它在 profile 里默认 disabled);
  • 所以"新开一条对话"只会重新挂载 preset,插件的模块本身仍是进程里已缓存的旧代码;
  • scripts/*.mjs 是每次直接执行的脚本,不受影响,改完立即是新的。

只有改了 preset 时,新开对话就够了;改了插件 .js(含 client-host.js / settings.js)或宿主组成相关的文件(cordis.patch.yml、package.json 的 dsh.* 声明)都必须重启 dsh。

用法

模型侧调用(不是人机命令):

compact_agents(scope = "others" | "all" | "self" | "ids", ids?: string[], whenBusy = "queue" | "skip")
scope含义
others(默认)除调用者以外的所有活会话
all所有活会话,含调用者自己
self只压调用者自己 —— 没有子代理时也有效
ids只压 ids 里列出的会话

典型说法:

  • 「把其他会话都压一遍」→ scope: "others"
  • 「我这条对话也一起压」→ scope: "all"(你自己会被排队,本轮结束补压)
  • 「只压我自己」→ scope: "self"

模型没调用它? 工具描述里已写明"用户要求压缩上下文时立即调用、不要反问",但仍可能遇到模型选择先确认一下。最稳的说法是把工具名说出来:

调用 compact_agents,scope=all,把所有会话压一遍

工具本身与模型行为无关——只要它在工具清单里,点名调用必定执行。

返回每个目标一行,例如:

compact_agents: 3 compacted, 1 queued, 0 skipped, 0 failed (of 4 selected).
- sess_ab12: compacted, ~213,400 → ~49,800 tokens — shadowed surface 12-107
- sess_cd34: compacted, ~52,100 → ~9,040 tokens — shadowed surface 3-33
- sess_ef56: noop, ~0 tokens shadowed — nothing safely compactable (empty session, or one oversized retained unit)
- sess_gh78: queued — mid-turn; queued and will be compacted as soon as it goes idle

beforeTokens / afterTokens 是压缩前后用 ctx.tokenMeter 实测的表面估算;测不到时为 -1。

对话区的压缩提示

工具之外,任何压缩(包括阈值触发的自动压缩)都会在对话区留下一条可见提示 —— 形态就是框架自己 注入上下文时用的那种折叠行:

▸ 上下文注入 · dsh-compact-agents · 正在压缩上下文…(当前 213,400 tokens) · 触发线 ×0.35
▸ 上下文注入 · dsh-compact-agents · 上下文压缩完成:约 213,400 → 49,800 tokens,已遮蔽 37 个历史节点

compaction/start 是在摘要模型调用之前写入会话日志的,所以第一条提示正好出现在原本那段 什么都看不见的等待里。行配置 notice: false 可关闭(工具不受影响)。

折叠行末尾那个 触发线 ×0.35 是本会话实际生效的值。正常情况下它总是跟 preset 文件一致 —— 保存设置时插件会把新值热同步进运行中的会话;只有热同步进不去时才会多一行:

⚠️ preset 文件里现在是 ×0.5,本会话这个实例仍按 ×0.2(热同步没成功):新开一条对话才会用上新值。

被输出上限截断时自动续写

一轮以 turn/end{reason: {kind: 'max-tokens'}} 结束时(DeepSeek 的 finish_reason: 'length'), 插件会替用户发一句"继续",让对话自己走下去,而不是停在那里等人手动发:

▸ 上下文注入 · dsh-compact-agents · 上一轮被输出上限截断,已自动续写(1/2)
继续                                    ← 插件以用户身份发出的(和用户手打的一样)
  • 次数上限由行配置 maxAutoContinues 控制,默认 2;0 或 false 关闭。用满后不再续写, 改为提示"已停止自动续写",避免"截断 → 续写 → 又截断"无止境烧 token。
  • 任一轮正常结束即清零,所以额度是"连续"次数,不会长期耗尽。
  • 发出的是标准 user/message(source.kind: 'user' + 冻结 + 唯一 id),走的就是人在界面上发消息 的同一条路(agent.followup)—— 不是往会话表面塞一条不唤醒模型的消息。

为什么需要它:压缩后 preset 常会把下一个请求的输出预算压到很小的窗口来"重新锚定"。 若模型开着高推理,思考 token 与正文共享这份预算,很容易整份被思考吃光 → 正文 0 字被判截断。 详见设计说明 §7。

在「设置」里改这些参数

打开 设置 → 插件,会看到一张「压缩与自动续写」卡片:

DSH 设置 → 插件 页里的「压缩与自动续写」卡片:编号 1 是入口,2 是卡片标题,3 是立即生效的两个字段,4 是新建会话生效的三个字段,5 是底部的放弃修改与保存

这张卡片是纯参数卡片:本插件在界面上没有自己的按钮 —— 压缩与自动续写都是自动发生的, 卡片上的「重置」只是设置框架自带的还原入口,不会触发压缩。 DSH 里唯一的手动压缩入口是自带的人机命令 /compact(不是本插件);本插件提供的是模型侧工具 compact_agents,由模型调用,不是给人点的。

字段含义生效时机
压缩触发阈值比例0.35 = 上下文用到 350K 就自动压缩新建会话生效
压缩后保留比例压缩后按该比例留下最近的历史新建会话生效
受控阶段输出预算每次压缩后会重新进入的"受控阶段"里,单个请求的输出预算新建会话生效
压缩进度提示是否在对话区播报「正在压缩上下文…/压缩完成」立即生效
自动续写次数被输出上限截断时最多自动发几次"继续"(0 = 关闭)立即生效

卡片上还会标出哪些字段是你覆盖过的(可以单独"重置"回 preset 里的值)。

卡片本身长什么样,见上面那张标注图;图里的编号与右侧图例一一对应。docs/images 记录了这批图的来源与做法(真实界面截图 + 标注,不含任何个人信息),将来若要补别的截图,命名与插入位置也在那里写清。

卡片能出现的前提是宿主组成里有那一行(本包作为 profile bundle 被登记、进而插进宿主 Loader):只在 preset 里挂载的话,浏览器根本收不到 lib/client.js,症状是设置页里既没有命名空间也没有卡片、且毫无报错。所以首次安装后、以及任何改动宿主组成之后,都要重启 dsh(根因见设计说明 §8.4)。

设计要点(为什么分成两种生效时机):

  • 前三个值属于 preset 里的其它插件(compaction-basic 的 thresholdRatio/retainRatio、 tool-bootstrap 的 bootstrapMaxTokens),插件没法替它们改运行时策略,所以改的是 preset 文件本身。preset 的挂载会记录文件 stamp,stamp 变了就给之后新建的会话开新一代 ——所以不用重启 DSH,但已经在跑的会话不受影响。写盘前会落一份 .bak-compact-agents 备份,并用临时文件 + rename 原子替换;只改目标那一行,preset 里的注释与排版原样保留。
  • 后两个值是本插件自己的,会话事件发生时才读,所以改完立即生效。

settings: false 可以整体关掉这个设置面(工具与提示不受影响)。

每个参数到底在管什么

上面那张表只说了"字段是什么意思"。这一节说清这个参数到底在管什么、往哪边调会怎样。 机制层面的理由(为什么这样设计)见设计说明 §9。

先看一次压缩从头到尾发生了什么(以 1M 窗口 ≈ 100 万 tokens、preset 用 0.35 / 0.05 为例):

压之前   系统提示 1 万 + 历史 34 万 = 35 万
         └─ 撞到 thresholdRatio 0.35 的触发线 → 开始压缩

压缩中   把"最近 5 万"以外的部分【遮蔽】掉(不是删除,是移出发送内容)
         └─ 换成一段摘要;压缩提示播报里报的"遮蔽节点数"就是它

压之后   系统提示 1 万 + 摘要 0.3 万 + 最近原文 5 万 ≈ 6.3 万
         └─ 这"最近 5 万"由 retainRatio 0.05 × 窗口 100 万 决定

随后     进入【受控阶段】:接下来每次请求最多输出 bootstrapMaxTokens tokens

三个数各管一段,互不重叠:thresholdRatio 管什么时候压,retainRatio 管压完留下多少原文, bootstrapMaxTokens 管压完那几轮最多让它说多少。没有一个参数管"摘要写得好不好"——那是摘要模型的职责。

把上面这条链路画成图,就是一次压缩的全过程(数值与文字版一致):

flowchart TD
    A["压缩前:系统提示 1 万 + 历史 34 万
≈ 35 万 tokens"] --> B["撞到 thresholdRatio 0.35 的触发线
(1M 窗口 × 0.35 = 35 万)"]
    B --> C["压缩:把「最近 5 万」以外的部分
遮蔽成一段摘要(不删除,只移出发送内容)"]
    C --> D["压缩后:系统提示 1 万 + 摘要 0.3 万 + 最近原文 5 万
≈ 6.3 万 tokens"]
    D --> E["「最近 5 万」= retainRatio 0.05 × 1M 窗口"]
    D --> F["进入受控阶段:
此后每次请求最多输出 bootstrapMaxTokens"]

主线只有一条:达线 → 遮蔽成摘要 → 进入受控阶段。下面按段拆开讲。

压缩后保留比例 retainRatio

本质是保真的边界:这条线以内的内容原文一字不差保留,线以外只剩摘要。

  • 它的单位是窗口比例,不是消息条数:0.05 × 1M = 保留最近 5 万 tokens 原文。
  • 为什么必须留一块原文:摘要一定丢细节;而最近发生的内容恰恰最可能马上要用(刚贴的代码、刚提的 需求、刚纠正的错误)。被总结掉就会出现"我刚说过的它当没看见"。
  • 调小的症状:0.01 = 只留 1 万,一个几百行的代码文件差不多就 1 万 tokens,压完立刻被总结掉 —— 模型转头就"忘"。
  • 调大的症状:0.2 = 留 20 万,压完还剩约 25 万,很快又撞触发线 → 反复压缩、反复打断。
  • 建议:一般对话 0.05 够用;经常贴大文件可调到 0.08~0.1。
受控阶段输出预算 bootstrapMaxTokens

它是输出预算,不是输入预算:压缩结束后那一小段时间里,每次请求最多让模型输出多少 tokens。

  • 最容易踩的坑:max_tokens 把思考(reasoning)token 也算在内。 所以 1024 时可能光思考就 吃满预算、正文一个字都出不来 —— 表现为"空回复"或话说到一半硬截断。(这正是本插件存在的起因之一: 此前观测到的 4 次截断全是 outputTokens=1024、正文 0 字。)
  • 为什么要"受控":刚压缩完,模型拿到一段崭新摘要,很容易一口气写一大篇,把刚清出来的空间又塞满, 前一次压缩就白做了。所以 preset 在每次压缩结束后把会话打回受控状态、强制卡住输出;等会话"晋升" (解除受控)后恢复模型本身的大预算。它是压缩之后的一段临时限流,不是永久设置。
  • 调小:16384 → 1024 省,但极易截断。
  • 调大:32768 不容易截断,但受控阶段每轮都可能很贵。
  • 与「自动续写」配套:预